From 210207a5fa856e0c8d53496ddd336c4e42a8c4ab Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Wed, 20 Nov 2024 08:26:49 +0100 Subject: [PATCH 01/44] feat(microsoft365): Add new provider microsoft 365 to prowler main --- prowler/__main__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/prowler/__main__.py b/prowler/__main__.py index 71325e1f4af..1952cfeeb18 100644 --- a/prowler/__main__.py +++ b/prowler/__main__.py @@ -76,6 +76,7 @@ from prowler.providers.common.quick_inventory import run_provider_quick_inventory from prowler.providers.gcp.models import GCPOutputOptions from prowler.providers.kubernetes.models import KubernetesOutputOptions +from prowler.providers.microsoft365.models import Microsoft365OutputOptions def prowler(): @@ -257,6 +258,10 @@ def prowler(): output_options = KubernetesOutputOptions( args, bulk_checks_metadata, global_provider.identity ) + elif provider == "microsoft365": + output_options = Microsoft365OutputOptions( + args, bulk_checks_metadata, global_provider.identity + ) # Run the quick inventory for the provider if available if hasattr(args, "quick_inventory") and args.quick_inventory: From 9bb4329b7f5c4314de2a648534c695391ddabc34 Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Wed, 20 Nov 2024 08:29:33 +0100 Subject: [PATCH 02/44] feat(microsoft365): Add microsoft365 CheckReport model --- prowler/lib/check/models.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index 446440b6aae..e3de21a64a0 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -482,6 +482,24 @@ def __init__(self, metadata): self.namespace = "" +@dataclass +class Check_Report_Microsoft365(Check_Report): + # TODO change class name to CheckReportMicrosoft365 + """Contains the Microsoft365 Check's finding information.""" + + resource_name: str + resource_id: str + subscription: str + location: str + + def __init__(self, metadata): + super().__init__(metadata) + self.resource_name = "" + self.resource_id = "" + self.subscription = "" + self.location = "global" + + # Testing Pending def load_check_metadata(metadata_file: str) -> CheckMetadata: """ From 9a9cc9a17a3341ace363aab641a29e10e3eb1e4f Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Wed, 20 Nov 2024 08:31:38 +0100 Subject: [PATCH 03/44] feat(microsoft365): Add microsoft365 to provider and summary table --- prowler/lib/outputs/summary_table.py | 3 +++ prowler/providers/common/provider.py | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/prowler/lib/outputs/summary_table.py b/prowler/lib/outputs/summary_table.py index a2090e53ef7..63d4480f80e 100644 --- a/prowler/lib/outputs/summary_table.py +++ b/prowler/lib/outputs/summary_table.py @@ -40,6 +40,9 @@ def display_summary_table( elif provider.type == "kubernetes": entity_type = "Context" audited_entities = provider.identity.context + elif provider.type == "microsoft365": + entity_type = "Tenant Domain" + audited_entities = provider.identity.tenant_domain # Check if there are findings and that they are not all MANUAL if findings and not all(finding.status == "MANUAL" for finding in findings): diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index 4e86ad42bbf..88bf32faa89 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -211,6 +211,13 @@ def init_global_provider(arguments: Namespace) -> None: mutelist_path=arguments.mutelist_file, fixer_config=fixer_config, ) + elif "microsoft365" in provider_class_name.lower(): + provider_class( + app_env_auth=arguments.app_env_auth, + config_path=arguments.config_file, + mutelist_path=arguments.mutelist_file, + fixer_config=fixer_config, + ) except TypeError as error: logger.critical( From a27029cd95f5596dfe2dafd2d67ac0590b00ffc3 Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Wed, 20 Nov 2024 08:34:57 +0100 Subject: [PATCH 04/44] feat(microsoft365): Add microsoft365 as a new provider. Add a service and a check to test if things are working properly --- .../microsoft365/cis_4.0_microsoft365.json | 30 ++ prowler/providers/microsoft365/__init__.py | 0 .../microsoft365/exceptions/exceptions.py | 301 ++++++++++++ .../providers/microsoft365/lib/__init__.py | 0 .../microsoft365/lib/arguments/__init__.py | 0 .../microsoft365/lib/arguments/arguments.py | 45 ++ .../microsoft365/lib/mutelist/__init__.py | 0 .../microsoft365/lib/mutelist/mutelist.py | 17 + .../microsoft365/lib/regions/__init__.py | 0 .../microsoft365/lib/regions/regions.py | 26 + .../microsoft365/lib/service/__init__.py | 0 .../microsoft365/lib/service/service.py | 33 ++ .../microsoft365/microsoft365_provider.py | 455 ++++++++++++++++++ prowler/providers/microsoft365/models.py | 50 ++ .../microsoft365/services/users/__init__.py | 0 ...strative_accounts_cloud_only.metadata.json | 30 ++ ...sers_administrative_accounts_cloud_only.py | 37 ++ .../services/users/users_client.py | 4 + .../services/users/users_service.py | 107 ++++ 19 files changed, 1135 insertions(+) create mode 100644 prowler/compliance/microsoft365/cis_4.0_microsoft365.json create mode 100644 prowler/providers/microsoft365/__init__.py create mode 100644 prowler/providers/microsoft365/exceptions/exceptions.py create mode 100644 prowler/providers/microsoft365/lib/__init__.py create mode 100644 prowler/providers/microsoft365/lib/arguments/__init__.py create mode 100644 prowler/providers/microsoft365/lib/arguments/arguments.py create mode 100644 prowler/providers/microsoft365/lib/mutelist/__init__.py create mode 100644 prowler/providers/microsoft365/lib/mutelist/mutelist.py create mode 100644 prowler/providers/microsoft365/lib/regions/__init__.py create mode 100644 prowler/providers/microsoft365/lib/regions/regions.py create mode 100644 prowler/providers/microsoft365/lib/service/__init__.py create mode 100644 prowler/providers/microsoft365/lib/service/service.py create mode 100644 prowler/providers/microsoft365/microsoft365_provider.py create mode 100644 prowler/providers/microsoft365/models.py create mode 100644 prowler/providers/microsoft365/services/users/__init__.py create mode 100644 prowler/providers/microsoft365/services/users/users_administrative_accounts_cloud_only/users_administrative_accounts_cloud_only.metadata.json create mode 100644 prowler/providers/microsoft365/services/users/users_administrative_accounts_cloud_only/users_administrative_accounts_cloud_only.py create mode 100644 prowler/providers/microsoft365/services/users/users_client.py create mode 100644 prowler/providers/microsoft365/services/users/users_service.py diff --git a/prowler/compliance/microsoft365/cis_4.0_microsoft365.json b/prowler/compliance/microsoft365/cis_4.0_microsoft365.json new file mode 100644 index 00000000000..10cfbd7c8fd --- /dev/null +++ b/prowler/compliance/microsoft365/cis_4.0_microsoft365.json @@ -0,0 +1,30 @@ +{ + "Framework": "CIS", + "Version": "4.0", + "Provider": "Microsoft365", + "Description": "The CIS Microsoft 365 Foundations Benchmark provides prescriptive guidance for establishing a secure configuration posture for Microsoft 365 Cloud offerings running on any OS.", + "Requirements": [ + { + "Id": "1.1.1", + "Description": "Ensure that 'Administrative accounts' are 'cloud-only'", + "Checks": [ + "entra_policy_ensure_default_user_cannot_create_tenants" + ], + "Attributes": [ + { + "Section": "1.Microsoft 365 admin center", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "", + "RationaleStatement": "", + "ImpactStatement": "", + "RemediationProcedure": "", + "AuditProcedure": "", + "AdditionalInformation": "", + "DefaultValue": "", + "References": "" + } + ] + } + ] +} diff --git a/prowler/providers/microsoft365/__init__.py b/prowler/providers/microsoft365/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/exceptions/exceptions.py b/prowler/providers/microsoft365/exceptions/exceptions.py new file mode 100644 index 00000000000..4cbb617a7d2 --- /dev/null +++ b/prowler/providers/microsoft365/exceptions/exceptions.py @@ -0,0 +1,301 @@ +from prowler.exceptions.exceptions import ProwlerException + + +# Exceptions codes from 2000 to 2999 are reserved for Microsoft365 exceptions +class Microsoft365BaseException(ProwlerException): + """Base class for Microsoft365 Errors.""" + + AZURE_ERROR_CODES = { + (2000, "Microsoft365EnvironmentVariableError"): { + "message": "Microsoft365 environment variable error", + "remediation": "Check the Microsoft365 environment variables and ensure they are properly set.", + }, + (2001, "Microsoft365NoSubscriptionsError"): { + "message": "No Microsoft365 subscriptions found", + "remediation": "Check the Microsoft365 subscriptions and ensure they are properly set up.", + }, + (2002, "Microsoft365SetUpIdentityError"): { + "message": "Microsoft365 identity setup error related with credentials", + "remediation": "Check credentials and ensure they are properly set up for Microsoft365 and the identity provider.", + }, + (2003, "Microsoft365NoAuthenticationMethodError"): { + "message": "No Microsoft365 authentication method found", + "remediation": "Check that any authentication method is properly set up for Microsoft365.", + }, + (2004, "Microsoft365BrowserAuthNoTenantIDError"): { + "message": "Microsoft365 browser authentication error: no tenant ID found", + "remediation": "To use browser authentication, ensure the tenant ID is properly set.", + }, + (2005, "Microsoft365TenantIDNoBrowserAuthError"): { + "message": "Microsoft365 tenant ID error: browser authentication not found", + "remediation": "To use browser authentication, both the tenant ID and browser authentication must be properly set.", + }, + (2006, "Microsoft365ArgumentTypeValidationError"): { + "message": "Microsoft365 argument type validation error", + "remediation": "Check the provided argument types specific to Microsoft365 and ensure they meet the required format.", + }, + (2007, "Microsoft365SetUpRegionConfigError"): { + "message": "Microsoft365 region configuration setup error", + "remediation": "Check the Microsoft365 region configuration and ensure it is properly set up.", + }, + (2008, "Microsoft365DefaultMicrosoft365CredentialError"): { + "message": "Error in DefaultMicrosoft365Credential", + "remediation": "Check that all the attributes are properly set up for the DefaultMicrosoft365Credential.", + }, + (2009, "Microsoft365InteractiveBrowserCredentialError"): { + "message": "Error retrieving InteractiveBrowserCredential", + "remediation": "Check your browser and ensure that the tenant ID and browser authentication are properly set.", + }, + (2010, "Microsoft365HTTPResponseError"): { + "message": "Error in HTTP response from Microsoft365", + "remediation": "", + }, + (2011, "Microsoft365CredentialsUnavailableError"): { + "message": "Error trying to configure Microsoft365 credentials because they are unavailable", + "remediation": "Check the dictionary and ensure it is properly set up for Microsoft365 credentials. TENANT_ID, CLIENT_ID and CLIENT_SECRET are required.", + }, + (2012, "Microsoft365GetTokenIdentityError"): { + "message": "Error trying to get token from Microsoft365 Identity", + "remediation": "Check the Microsoft365 Identity and ensure it is properly set up.", + }, + (2013, "Microsoft365NotTenantIdButClientIdAndClienSecretError"): { + "message": "The provided credentials are not a tenant ID but a client ID and client secret", + "remediation": "Tenant Id, Client Id and Client Secret are required for Microsoft365 credentials. Make sure you are using the correct credentials.", + }, + (2014, "Microsoft365ClientAuthenticationError"): { + "message": "Error in client authentication", + "remediation": "Check the client authentication and ensure it is properly set up.", + }, + (2015, "Microsoft365SetUpSessionError"): { + "message": "Error setting up session", + "remediation": "Check the session setup and ensure it is properly set up.", + }, + (2016, "Microsoft365NotValidTenantIdError"): { + "message": "The provided tenant ID is not valid", + "remediation": "Check the tenant ID and ensure it is a valid ID.", + }, + (2017, "Microsoft365NotValidClientIdError"): { + "message": "The provided client ID is not valid", + "remediation": "Check the client ID and ensure it is a valid ID.", + }, + (2018, "Microsoft365NotValidClientSecretError"): { + "message": "The provided client secret is not valid", + "remediation": "Check the client secret and ensure it is a valid secret.", + }, + (2019, "Microsoft365ConfigCredentialsError"): { + "message": "Error in configuration of Microsoft365 credentials", + "remediation": "Check the configuration of Microsoft365 credentials and ensure it is properly set up.", + }, + (2020, "Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError"): { + "message": "The provided client ID and client secret do not belong to the provided tenant ID", + "remediation": "Check the client ID and client secret and ensure they belong to the provided tenant ID.", + }, + (2021, "Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError"): { + "message": "The provided tenant ID and client secret do not belong to the provided client ID", + "remediation": "Check the tenant ID and client secret and ensure they belong to the provided client ID.", + }, + (2022, "Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError"): { + "message": "The provided tenant ID and client ID do not belong to the provided client secret", + "remediation": "Check the tenant ID and client ID and ensure they belong to the provided client secret.", + }, + (2023, "Microsoft365InvalidProviderIdError"): { + "message": "The provided provider_id does not match with the available subscriptions", + "remediation": "Check the provider_id and ensure it is a valid subscription for the given credentials.", + }, + } + + def __init__(self, code, file=None, original_exception=None, message=None): + provider = "Microsoft365" + error_info = self.AZURE_ERROR_CODES.get((code, self.__class__.__name__)) + if message: + error_info["message"] = message + super().__init__( + code=code, + source=provider, + file=file, + original_exception=original_exception, + error_info=error_info, + ) + + +class Microsoft365CredentialsError(Microsoft365BaseException): + """Base class for Microsoft365 credentials errors.""" + + def __init__(self, code, file=None, original_exception=None, message=None): + super().__init__(code, file, original_exception, message) + + +class Microsoft365EnvironmentVariableError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2000, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365NoSubscriptionsError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2001, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365SetUpIdentityError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2002, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365NoAuthenticationMethodError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2003, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365BrowserAuthNoTenantIDError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2004, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365TenantIDNoBrowserAuthError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2005, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365ArgumentTypeValidationError(Microsoft365BaseException): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2006, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365SetUpRegionConfigError(Microsoft365BaseException): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2007, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365DefaultMicrosoft365CredentialError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2008, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365InteractiveBrowserCredentialError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2009, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365HTTPResponseError(Microsoft365BaseException): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2010, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365CredentialsUnavailableError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2011, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365GetTokenIdentityError(Microsoft365BaseException): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2012, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365NotTenantIdButClientIdAndClienSecretError( + Microsoft365CredentialsError +): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2013, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365ClientAuthenticationError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2014, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365SetUpSessionError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2015, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365NotValidTenantIdError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2016, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365NotValidClientIdError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2017, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365NotValidClientSecretError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2018, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365ConfigCredentialsError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2019, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError( + Microsoft365CredentialsError +): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2020, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError( + Microsoft365CredentialsError +): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2021, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError( + Microsoft365CredentialsError +): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2022, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365InvalidProviderIdError(Microsoft365BaseException): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 2023, file=file, original_exception=original_exception, message=message + ) diff --git a/prowler/providers/microsoft365/lib/__init__.py b/prowler/providers/microsoft365/lib/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/lib/arguments/__init__.py b/prowler/providers/microsoft365/lib/arguments/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/lib/arguments/arguments.py b/prowler/providers/microsoft365/lib/arguments/arguments.py new file mode 100644 index 00000000000..b614cbb9fbd --- /dev/null +++ b/prowler/providers/microsoft365/lib/arguments/arguments.py @@ -0,0 +1,45 @@ +from argparse import ArgumentTypeError + + +def init_parser(self): + """Init the Microsoft365 Provider CLI parser""" + microsoft365_parser = self.subparsers.add_parser( + "microsoft365", + parents=[self.common_providers_parser], + help="Microsoft365 Provider", + ) + # Authentication Modes + microsoft365_auth_subparser = microsoft365_parser.add_argument_group( + "Authentication Modes" + ) + microsoft365_auth_modes_group = ( + microsoft365_auth_subparser.add_mutually_exclusive_group() + ) + microsoft365_auth_modes_group.add_argument( + "--app-env-auth", + action="store_true", + help="Use application environment variables authentication to log in against Microsoft 365", + ) + # Regions + microsoft365_regions_subparser = microsoft365_parser.add_argument_group("Regions") + microsoft365_regions_subparser.add_argument( + "--microsoft365-region", + nargs="?", + default="AzureCloud", + type=validate_microsoft365_region, + help="microsoft365 region from `az cloud list --output table`, by default AzureCloud", + ) + + +def validate_microsoft365_region(region): + """validate_microsoft365_region validates if the region passed as argument is valid""" + regions_allowed = [ + "AzureChinaCloud", + "AzureUSGovernment", + "AzureCloud", + ] + if region not in regions_allowed: + raise ArgumentTypeError( + f"Region {region} not allowed, allowed regions are {' '.join(regions_allowed)}" + ) + return region diff --git a/prowler/providers/microsoft365/lib/mutelist/__init__.py b/prowler/providers/microsoft365/lib/mutelist/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/lib/mutelist/mutelist.py b/prowler/providers/microsoft365/lib/mutelist/mutelist.py new file mode 100644 index 00000000000..85b61fc06da --- /dev/null +++ b/prowler/providers/microsoft365/lib/mutelist/mutelist.py @@ -0,0 +1,17 @@ +from prowler.lib.check.models import Check_Report_Microsoft365 +from prowler.lib.mutelist.mutelist import Mutelist +from prowler.lib.outputs.utils import unroll_dict, unroll_tags + + +class Microsoft365Mutelist(Mutelist): + def is_finding_muted( + self, + finding: Check_Report_Microsoft365, + cluster: str, + ) -> bool: + return self.is_muted( + cluster, + finding.check_metadata.CheckID, + finding.resource_name, + unroll_dict(unroll_tags(finding.resource_tags)), + ) diff --git a/prowler/providers/microsoft365/lib/regions/__init__.py b/prowler/providers/microsoft365/lib/regions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/lib/regions/regions.py b/prowler/providers/microsoft365/lib/regions/regions.py new file mode 100644 index 00000000000..6b88ab5561a --- /dev/null +++ b/prowler/providers/microsoft365/lib/regions/regions.py @@ -0,0 +1,26 @@ +from azure.identity import AzureAuthorityHosts + +AZURE_CHINA_CLOUD = "https://management.chinacloudapi.cn" +AZURE_US_GOV_CLOUD = "https://management.usgovcloudapi.net" +AZURE_GENERIC_CLOUD = "https://management.azure.com" + + +def get_regions_config(region): + allowed_regions = { + "AzureCloud": { + "authority": None, + "base_url": AZURE_GENERIC_CLOUD, + "credential_scopes": [AZURE_GENERIC_CLOUD + "/.default"], + }, + "AzureChinaCloud": { + "authority": AzureAuthorityHosts.AZURE_CHINA, + "base_url": AZURE_CHINA_CLOUD, + "credential_scopes": [AZURE_CHINA_CLOUD + "/.default"], + }, + "AzureUSGovernment": { + "authority": AzureAuthorityHosts.AZURE_GOVERNMENT, + "base_url": AZURE_US_GOV_CLOUD, + "credential_scopes": [AZURE_US_GOV_CLOUD + "/.default"], + }, + } + return allowed_regions[region] diff --git a/prowler/providers/microsoft365/lib/service/__init__.py b/prowler/providers/microsoft365/lib/service/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/lib/service/service.py b/prowler/providers/microsoft365/lib/service/service.py new file mode 100644 index 00000000000..2f4b7269505 --- /dev/null +++ b/prowler/providers/microsoft365/lib/service/service.py @@ -0,0 +1,33 @@ +from msgraph import GraphServiceClient + +from prowler.lib.logger import logger +from prowler.providers.microsoft365.microsoft365_provider import Microsoft365Provider + + +class Microsoft365Service: + def __init__( + self, + provider: Microsoft365Provider, + ): + self.clients = self.__set_clients__( + provider.identity, + provider.session, + provider.region_config, + ) + + self.locations = provider.locations + self.audit_config = provider.audit_config + self.fixer_config = provider.fixer_config + + def __set_clients__(self, identity, session, region_config): + clients = {} + try: + clients.update( + {identity.tenant_domain: GraphServiceClient(credentials=session)} + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + else: + return clients diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py new file mode 100644 index 00000000000..9f424721ef3 --- /dev/null +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -0,0 +1,455 @@ +import asyncio +import os +from argparse import ArgumentTypeError +from os import getenv + +import requests +from azure.core.exceptions import ClientAuthenticationError, HttpResponseError +from azure.identity import ClientSecretCredential, DefaultAzureCredential +from colorama import Fore, Style +from msgraph import GraphServiceClient + +from prowler.config.config import ( + default_config_file_path, + get_default_mute_file_path, + load_and_validate_config_file, +) +from prowler.lib.logger import logger +from prowler.lib.utils.utils import print_boxes +from prowler.providers.common.models import Audit_Metadata +from prowler.providers.common.provider import Provider +from prowler.providers.microsoft365.exceptions.exceptions import ( + Microsoft365ArgumentTypeValidationError, + Microsoft365CredentialsUnavailableError, + Microsoft365EnvironmentVariableError, + Microsoft365GetTokenIdentityError, + Microsoft365HTTPResponseError, + Microsoft365SetUpRegionConfigError, +) +from prowler.providers.microsoft365.lib.arguments.arguments import ( + validate_microsoft365_region, +) +from prowler.providers.microsoft365.lib.mutelist.mutelist import Microsoft365Mutelist +from prowler.providers.microsoft365.lib.regions.regions import get_regions_config +from prowler.providers.microsoft365.models import ( + Microsoft365IdentityInfo, + Microsoft365RegionConfig, +) + + +class Microsoft365Provider(Provider): + """ + Represents an Microsoft365 provider. + + This class provides functionality to interact with the Microsoft365 resources. + It handles authentication, region configuration, and provides access to various properties and methods + related to the Microsoft365 provider. + + Attributes: + _type (str): The type of the provider, which is set to "microsoft365". + _session (DefaultMicrosoft365Credential): The session object associated with the Microsoft365 provider. + _identity (Microsoft365IdentityInfo): The identity information for the Microsoft365 provider. + _audit_config (dict): The audit configuration for the Microsoft365 provider. + _region_config (Microsoft365RegionConfig): The region configuration for the Microsoft365 provider. + _locations (dict): A dictionary containing the available locations for the Microsoft365 provider. + _mutelist (Microsoft365Mutelist): The mutelist object associated with the Microsoft365 provider. + audit_metadata (Audit_Metadata): The audit metadata for the Microsoft365 provider. + + Methods: + __init__ -> Initializes the Microsoft365 provider. + identity(self): Returns the identity of the Microsoft365 provider. + type(self): Returns the type of the Microsoft365 provider. + session(self): Returns the session object associated with the Microsoft365 provider. + region_config(self): Returns the region configuration for the Microsoft365 provider. + locations(self): Returns a list of available locations for the Microsoft365 provider. + audit_config(self): Returns the audit configuration for the Microsoft365 provider. + fixer_config(self): Returns the fixer configuration. + output_options(self, options: tuple): Sets the output options for the Microsoft365 provider. + mutelist(self) -> Microsoft365Mutelist: Returns the mutelist object associated with the Microsoft365 provider. + validate_arguments(cls, az_cli_auth, app_env_auth, browser_auth, managed_identity_auth, tenant_id): Validates the authentication arguments for the Microsoft365 provider. + setup_region_config(cls, region): Sets up the region configuration for the Microsoft365 provider. + print_credentials(self): Prints the Microsoft365 credentials information. + setup_session(cls, az_cli_auth, app_env_auth, browser_auth, managed_identity_auth, tenant_id, region_config): Set up the Microsoft365 session with the specified authentication method. + """ + + _type: str = "microsoft365" + _session: DefaultAzureCredential + _identity: Microsoft365IdentityInfo + _audit_config: dict + _region_config: Microsoft365RegionConfig + _locations: dict + _mutelist: Microsoft365Mutelist + # TODO: this is not optional, enforce for all providers + audit_metadata: Audit_Metadata + + def __init__( + self, + app_env_auth: bool = False, + tenant_id: str = None, + region: str = "AzureCloud", + client_id: str = None, + client_secret: str = None, + config_content: dict = None, + config_path: str = None, + mutelist_path: str = None, + mutelist_content: dict = None, + fixer_config: dict = {}, + ): + """ + Initializes the Microsoft365 provider. + + Args: + app_env_auth (bool): Flag indicating whether to use application authentication with environment variables. + tenant_id (str): The Microsoft365 Active Directory tenant ID. + region (str): The Microsoft365 region. + client_id (str): The Microsoft365 client ID. + client_secret (str): The Microsoft365 client secret. + config_path (str): The path to the configuration file. + config_content (dict): The configuration content. + fixer_config (dict): The fixer configuration. + mutelist_path (str): The path to the mutelist file. + mutelist_content (dict): The mutelist content. + + Returns: + None + + Raises: + Microsoft365ArgumentTypeValidationError: If there is an error in the argument type validation. + Microsoft365SetUpRegionConfigError: If there is an error in setting up the region configuration. + Microsoft365DefaultMicrosoft365CredentialError: If there is an error in retrieving the Microsoft365 credentials. + Microsoft365InteractiveBrowserCredentialError: If there is an error in retrieving the Microsoft365 credentials using browser authentication. + Microsoft365ConfigCredentialsError: If there is an error in configuring the Microsoft365 credentials from a dictionary. + Microsoft365GetTokenIdentityError: If there is an error in getting the token from the Microsoft365 identity. + Microsoft365HTTPResponseError: If there is an HTTP response error. + """ + logger.info("Setting Microsoft365 provider ...") + + logger.info("Checking if any credentials mode is set ...") + + logger.info("Checking if region is different than default one") + self._region_config = self.setup_region_config(region) + + # Set up the Microsoft365 session + self._session = self.setup_session( + app_env_auth, + ) + + # Set up the identity + self._identity = self.setup_identity( + app_env_auth, + ) + + # TODO: should we keep this here or within the identity? + self._locations = self.get_locations(self.session) + + # Audit Config + if config_content: + self._audit_config = config_content + else: + if not config_path: + config_path = default_config_file_path + self._audit_config = load_and_validate_config_file(self._type, config_path) + + # Fixer Config + self._fixer_config = fixer_config + + # Mutelist + if mutelist_content: + self._mutelist = Microsoft365Mutelist( + mutelist_content=mutelist_content, + ) + else: + if not mutelist_path: + mutelist_path = get_default_mute_file_path(self.type) + self._mutelist = Microsoft365Mutelist( + mutelist_path=mutelist_path, + ) + + Provider.set_global_provider(self) + + @property + def identity(self): + """Returns the identity of the Microsoft365 provider.""" + return self._identity + + @property + def type(self): + """Returns the type of the Microsoft365 provider.""" + return self._type + + @property + def session(self): + """Returns the session object associated with the Microsoft365 provider.""" + return self._session + + @property + def region_config(self): + """Returns the region configuration for the Microsoft365 provider.""" + return self._region_config + + @property + def locations(self): + """Returns a list of available locations for the Microsoft365 provider.""" + return self._locations + + @property + def audit_config(self): + """Returns the audit configuration for the Microsoft365 provider.""" + return self._audit_config + + @property + def fixer_config(self): + """Returns the fixer configuration.""" + return self._fixer_config + + @property + def mutelist(self) -> Microsoft365Mutelist: + """Mutelist object associated with this Microsoft365 provider.""" + return self._mutelist + + @staticmethod + def setup_region_config(region): + """ + Sets up the region configuration for the Microsoft365 provider. + + Args: + region (str): The name of the region. + + Returns: + Microsoft365RegionConfig: The region configuration object. + + """ + try: + validate_microsoft365_region(region) + config = get_regions_config(region) + + return Microsoft365RegionConfig( + name=region, + authority=config["authority"], + base_url=config["base_url"], + credential_scopes=config["credential_scopes"], + ) + except ArgumentTypeError as validation_error: + logger.error( + f"{validation_error.__class__.__name__}[{validation_error.__traceback__.tb_lineno}]: {validation_error}" + ) + raise Microsoft365ArgumentTypeValidationError( + file=os.path.basename(__file__), + original_exception=validation_error, + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + raise Microsoft365SetUpRegionConfigError( + file=os.path.basename(__file__), + original_exception=error, + ) + + def print_credentials(self): + """Microsoft365 credentials information. + + This method prints the Microsoft365 Tenant Domain, Microsoft365 Tenant ID, Microsoft365 Region, + Microsoft365 Subscriptions, Microsoft365 Identity Type, and Microsoft365 Identity ID. + + Args: + None + + Returns: + None + """ + printed_subscriptions = [] + for key, value in self._identity.subscriptions.items(): + intermediate = key + ": " + value + printed_subscriptions.append(intermediate) + report_lines = [ + f"Microsoft365 Region: {Fore.YELLOW}{self.region_config.name}{Style.RESET_ALL}", + f"Microsoft365 Identity Type: {Fore.YELLOW}{self._identity.identity_type}{Style.RESET_ALL} Microsoft365 Identity ID: {Fore.YELLOW}{self._identity.identity_id}{Style.RESET_ALL}", + ] + report_title = ( + f"{Style.BRIGHT}Using the Azure credentials below:{Style.RESET_ALL}" + ) + print_boxes(report_lines, report_title) + + # TODO: setup_session or setup_credentials? + # This should be setup_credentials, since it is setting up the credentials for the provider + @staticmethod + def setup_session( + app_env_auth: bool, + ): + """Returns the Microsoft365 credentials object. + + Set up the Microsoft365 session with the specified authentication method. + + Args: + app_env_auth (bool): Flag indicating whether to use application authentication with environment variables. + + Returns: + credentials: The Microsoft365 credentials object. + + Raises: + Exception: If failed to retrieve Microsoft365 credentials. + + """ + # Browser auth creds cannot be set with DefaultMicrosoft365Credentials() + if app_env_auth: + try: + Microsoft365Provider.check_application_creds_env_vars() + credentials = ClientSecretCredential( + client_id=getenv("APP_CLIENT_ID"), + tenant_id=getenv("APP_TENANT_ID"), + client_secret=getenv("APP_CLIENT_SECRET"), + ) + except ( + Microsoft365EnvironmentVariableError + ) as environment_credentials_error: + logger.critical( + f"{environment_credentials_error.__class__.__name__}[{environment_credentials_error.__traceback__.tb_lineno}] -- {environment_credentials_error}" + ) + raise environment_credentials_error + if not credentials: + raise Microsoft365CredentialsUnavailableError( + file=os.path.basename(__file__), + message="Failed to retrieve Microsoft365 credentials.", + ) + return credentials + + @staticmethod + def check_application_creds_env_vars(): + """ + Checks the presence of required environment variables for application authentication against Azure. + + This method checks for the presence of the following environment variables: + - APP_CLIENT_ID: Microsoft365 client ID + - APP_TENANT_ID: Microsoft365 tenant ID + - APP_CLIENT_SECRET: Microsoft365 client secret + + If any of the environment variables is missing, it logs a critical error and exits the program. + """ + logger.info( + "Microsoft365 provider: checking service principal environment variables ..." + ) + for env_var in ["APP_CLIENT_ID", "APP_TENANT_ID", "APP_CLIENT_SECRET"]: + if not getenv(env_var): + logger.critical( + f"Microsoft365 provider: Missing environment variable {env_var} needed to authenticate against Microsoft365" + ) + raise Microsoft365EnvironmentVariableError( + file=os.path.basename(__file__), + message=f"Missing environment variable {env_var} required to authenticate.", + ) + + def setup_identity( + self, + app_env_auth, + ): + """ + Sets up the identity for the Microsoft365 provider. + + Args: + app_env_auth (bool): Flag indicating if Service Principal environment authentication is used. + + Returns: + Microsoft365IdentityInfo: An instance of Microsoft365IdentityInfo containing the identity information. + """ + credentials = self.session + # TODO: fill this object with real values not default and set to none + identity = Microsoft365IdentityInfo() + + # If credentials comes from service principal or browser, if the required permissions are assigned + # the identity can access AAD and retrieve the tenant domain name. + # With cli also should be possible but right now it does not work, microsoft365 python package issue is coming + # At the time of writting this with az cli creds is not working, despite that is included + if app_env_auth: + + async def get_microsoft365_identity(): + # Trying to recover tenant domain info + try: + logger.info( + "Trying to retrieve tenant domain from AAD to populate identity structure ..." + ) + client = GraphServiceClient(credentials=credentials) + + domain_result = await client.domains.get() + if getattr(domain_result, "value"): + if getattr(domain_result.value[0], "id"): + identity.tenant_domain = domain_result.value[0].id + + except HttpResponseError as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365HTTPResponseError( + file=os.path.basename(__file__), + original_exception=error, + ) + except ClientAuthenticationError as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365GetTokenIdentityError( + file=os.path.basename(__file__), + original_exception=error, + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + # since that exception is not considered as critical, we keep filling another identity fields + if app_env_auth: + # The id of the sp can be retrieved from environment variables + identity.identity_id = getenv("APP_CLIENT_ID") + identity.identity_type = "Application" + # Same here, if user can access AAD, some fields are retrieved if not, default value, for az cli + # should work but it doesn't, pending issue + else: + identity.identity_id = "Unknown user id (Missing AAD permissions)" + identity.identity_type = "User" + try: + logger.info( + "Trying to retrieve user information from AAD to populate identity structure ..." + ) + client = GraphServiceClient(credentials=credentials) + + me = await client.me.get() + if me: + if getattr(me, "user_principal_name"): + identity.identity_id = me.user_principal_name + + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + + asyncio.get_event_loop().run_until_complete(get_microsoft365_identity()) + + return identity + + def get_locations(self, credentials) -> dict[str, list[str]]: + """ + Retrieves the locations available for each subscription using the provided credentials. + + Args: + credentials: The credentials object used to authenticate the request. + + Returns: + A dictionary containing the locations available for each subscription. The dictionary + has subscription display names as keys and lists of location names as values. + """ + locations = None + if credentials: + locations = {} + token = credentials.get_token("https://management.azure.com/.default").token + for display_name, subscription_id in self._identity.subscriptions.items(): + locations.update({display_name: []}) + url = f"https://management.azure.com/subscriptions/{subscription_id}/locations?api-version=2022-12-01" + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } + response = requests.get(url, headers=headers) + if response.status_code == 200: + data = response.json() + for location in data["value"]: + locations[display_name].append(location["name"]) + return locations diff --git a/prowler/providers/microsoft365/models.py b/prowler/providers/microsoft365/models.py new file mode 100644 index 00000000000..30672926188 --- /dev/null +++ b/prowler/providers/microsoft365/models.py @@ -0,0 +1,50 @@ +from pydantic import BaseModel + +from prowler.config.config import output_file_timestamp +from prowler.providers.common.models import ProviderOutputOptions + + +class Microsoft365IdentityInfo(BaseModel): + identity_id: str = "" + identity_type: str = "" + tenant_ids: list[str] = [] + tenant_domain: str = "Unknown tenant domain (missing AAD permissions)" + subscriptions: dict = {} + locations: dict = {} + + +class Microsoft365RegionConfig(BaseModel): + name: str = "" + authority: str = None + base_url: str = "" + credential_scopes: list = [] + + +class Microsoft365Subscription(BaseModel): + id: str + subscription_id: str + display_name: str + state: str + + +class Microsoft365OutputOptions(ProviderOutputOptions): + def __init__(self, arguments, bulk_checks_metadata, identity): + # First call Provider_Output_Options init + super().__init__(arguments, bulk_checks_metadata) + + # Check if custom output filename was input, if not, set the default + if ( + not hasattr(arguments, "output_filename") + or arguments.output_filename is None + ): + if ( + identity.tenant_domain + != "Unknown tenant domain (missing AAD permissions)" + ): + self.output_filename = ( + f"prowler-output-{identity.tenant_domain}-{output_file_timestamp}" + ) + else: + self.output_filename = f"prowler-output-{'-'.join(identity.tenant_ids)}-{output_file_timestamp}" + else: + self.output_filename = arguments.output_filename diff --git a/prowler/providers/microsoft365/services/users/__init__.py b/prowler/providers/microsoft365/services/users/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/services/users/users_administrative_accounts_cloud_only/users_administrative_accounts_cloud_only.metadata.json b/prowler/providers/microsoft365/services/users/users_administrative_accounts_cloud_only/users_administrative_accounts_cloud_only.metadata.json new file mode 100644 index 00000000000..6a6d0b02022 --- /dev/null +++ b/prowler/providers/microsoft365/services/users/users_administrative_accounts_cloud_only/users_administrative_accounts_cloud_only.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "microsoft365", + "CheckID": "users_administrative_accounts_cloud_only", + "CheckTitle": "Ensure Administrative accounts are cloud-only", + "CheckType": [], + "ServiceName": "users", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "AdministrativeAccount", + "Description": "Administrative accounts must be cloud-only and separated from on-premises accounts. These accounts should not have applications assigned to them and should be used exclusively for administrative tasks.", + "Risk": "Failing to separate administrative accounts can lead to compromised security in hybrid environments. A breach in the cloud could potentially impact the on-premises environment and vice versa.", + "RelatedUrl": "https://learn.microsoft.com/en-us/microsoft-365/security/identity-protection?view=o365-worldwide", + "Remediation": { + "Code": { + "CLI": "Get-MsolUser -Admin | Where-Object {$_.ImmutableId -ne $null} | Remove-MsolUser", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Create cloud-only administrative accounts and ensure they are not synchronized from on-premises directories. Remove any unnecessary application assignments.", + "Url": "https://learn.microsoft.com/en-us/azure/active-directory/roles/security-design-administrative-accounts" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Administrative accounts should be strictly cloud-only and dedicated to admin tasks. Migrate all necessary permissions, including M365 and Azure RBAC roles, to these accounts." +} diff --git a/prowler/providers/microsoft365/services/users/users_administrative_accounts_cloud_only/users_administrative_accounts_cloud_only.py b/prowler/providers/microsoft365/services/users/users_administrative_accounts_cloud_only/users_administrative_accounts_cloud_only.py new file mode 100644 index 00000000000..ea21206aedc --- /dev/null +++ b/prowler/providers/microsoft365/services/users/users_administrative_accounts_cloud_only/users_administrative_accounts_cloud_only.py @@ -0,0 +1,37 @@ +from prowler.lib.check.models import Check, Check_Report_Microsoft365 +from prowler.providers.microsoft365.services.users.users_client import users_client + + +class users_administrative_accounts_cloud_only(Check): + def execute(self) -> Check_Report_Microsoft365: + findings = [] + + for tenant_domain, directory_roles in users_client.directory_roles.items(): + for role_name, directory_role in directory_roles.items(): + report = Check_Report_Microsoft365(self.metadata()) + report.subscription = f"Tenant: {tenant_domain}" + report.resource_name = role_name + report.resource_id = directory_role.id + report.status = "PASS" + + non_compliant_members = [ + member + for member in directory_roles.members + if member.on_premises_sync_enabled + ] + + if non_compliant_members: + report.status = "FAIL" + report.status_extended = ( + f"The following administrators in role '{role_name}' " + f"are synchronized with on-premises: " + f"{', '.join([member.name for member in non_compliant_members])}." + ) + else: + report.status_extended = ( + f"All administrators in role '{role_name}' are cloud-only." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/microsoft365/services/users/users_client.py b/prowler/providers/microsoft365/services/users/users_client.py new file mode 100644 index 00000000000..cc9cd821a43 --- /dev/null +++ b/prowler/providers/microsoft365/services/users/users_client.py @@ -0,0 +1,4 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.microsoft365.services.users.users_service import Users + +users_client = Users(Provider.get_global_provider()) diff --git a/prowler/providers/microsoft365/services/users/users_service.py b/prowler/providers/microsoft365/services/users/users_service.py new file mode 100644 index 00000000000..25d2c019472 --- /dev/null +++ b/prowler/providers/microsoft365/services/users/users_service.py @@ -0,0 +1,107 @@ +from asyncio import gather, get_event_loop +from typing import List, Optional + +from pydantic import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.microsoft365.lib.service.service import Microsoft365Service +from prowler.providers.microsoft365.microsoft365_provider import Microsoft365Provider + + +class Users(Microsoft365Service): + def __init__(self, provider: Microsoft365Provider): + super().__init__(provider) + + loop = get_event_loop() + + # Get users first alone because it is a dependency for other attributes + self.users = loop.run_until_complete(self._get_users()) + + attributes = loop.run_until_complete( + gather( + self._get_directory_roles(), + ) + ) + + self.directory_roles = attributes[0] + + async def _get_users(self): + logger.info("Entra - Getting users...") + users = {} + try: + for tenant, client in self.clients.items(): + users_list = await client.users.get( + params={ + "$select": "id,displayName,userPrincipalName,onPremisesSyncEnabled" + } + ) + users.update({tenant: {}}) + for user in users_list.value: + users[tenant].update( + { + user.user_principal_name: User( + id=user.id, + name=user.display_name, + on_premises_sync_enabled=user.on_premises_sync_enabled, + ) + } + ) + except Exception as error: + if ( + error.__class__.__name__ == "ODataError" + and error.__dict__.get("response_status_code", None) == 403 + ): + logger.error( + "You need 'UserAuthenticationMethod.Read.All' permission to access this information. It only can be granted through Service Principal authentication." + ) + else: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + return users + + async def _get_directory_roles(self): + logger.info("Entra - Getting directory roles...") + directory_roles_with_members = {} + try: + for tenant, client in self.clients.items(): + directory_roles_with_members.update({tenant: {}}) + directory_roles = await client.directory_roles.get() + for directory_role in directory_roles.value: + directory_role_members = ( + await client.directory_roles.by_directory_role_id( + directory_role.id + ).members.get() + ) + directory_roles_with_members[tenant].update( + { + directory_role.display_name: DirectoryRole( + id=directory_role.id, + members=[ + self.users[tenant][member.user_principal_name] + for member in directory_role_members.value + if self.users[tenant].get( + member.user_principal_name, None + ) + ], + ) + } + ) + + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return directory_roles_with_members + + +class User(BaseModel): + id: str + name: str + on_premises_sync_enabled: Optional[bool] = None + + +class DirectoryRole(BaseModel): + id: str + members: List[User] From ad1518f85a2234d3aaf85362f552ed69cc9ae90f Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Tue, 26 Nov 2024 09:28:56 +0100 Subject: [PATCH 05/44] feat(microsoft365): Fix some things so the output is correctly generated --- prowler/lib/check/models.py | 4 +- prowler/lib/outputs/finding.py | 14 ++ prowler/lib/outputs/html/html.py | 46 ++++++ prowler/lib/outputs/outputs.py | 2 + .../microsoft365/exceptions/exceptions.py | 18 +-- .../microsoft365/microsoft365_provider.py | 141 +++++++++++++++++- prowler/providers/microsoft365/models.py | 7 - 7 files changed, 208 insertions(+), 24 deletions(-) diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index e3de21a64a0..ceb345f814f 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -489,14 +489,14 @@ class Check_Report_Microsoft365(Check_Report): resource_name: str resource_id: str - subscription: str + tenant_id: str location: str def __init__(self, metadata): super().__init__(metadata) self.resource_name = "" self.resource_id = "" - self.subscription = "" + self.tenant_id = "" self.location = "global" diff --git a/prowler/lib/outputs/finding.py b/prowler/lib/outputs/finding.py index 9867111765c..da05e75844d 100644 --- a/prowler/lib/outputs/finding.py +++ b/prowler/lib/outputs/finding.py @@ -232,6 +232,20 @@ def generate_output( ) output_data["region"] = f"namespace: {check_output.namespace}" + elif provider.type == "microsoft365": + output_data["auth_method"] = ( + f"{provider.identity.identity_type}: {provider.identity.identity_id}" + ) + output_data["account_uid"] = get_nested_attribute( + provider, "identity.tenant_domain" + ) + output_data["account_name"] = get_nested_attribute( + provider, "identity.tenant_domain" + ) + output_data["resource_name"] = check_output.resource_name + output_data["resource_uid"] = check_output.resource_id + output_data["region"] = check_output.location + # check_output Unique ID # TODO: move this to a function # TODO: in Azure, GCP and K8s there are fidings without resource_name diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py index 5d5c4715935..6d2f49c603c 100644 --- a/prowler/lib/outputs/html/html.py +++ b/prowler/lib/outputs/html/html.py @@ -538,6 +538,52 @@ def get_kubernetes_assessment_summary(provider: Provider) -> str: ) return "" + @staticmethod + def get_microsoft365_assessment_summary(provider: Provider) -> str: + """ + get_microsoft365_assessment_summary gets the HTML assessment summary for the provider + + Args: + provider (Provider): the provider object + + Returns: + str: the HTML assessment summary + """ + try: + return f""" +
+
+
+ Azure Assessment Summary +
+
    +
  • + Microsoft365 Tenant Domain: {provider.identity.tenant_domain} +
  • +
+
+
+
+
+
+ Azure Credentials +
+
    +
  • + Azure Identity Type: {provider.identity.identity_type} +
  • +
  • + Azure Identity ID: {provider.identity.identity_id} +
  • +
+
+
""" + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + return "" + @staticmethod def get_assessment_summary(provider: Provider) -> str: """ diff --git a/prowler/lib/outputs/outputs.py b/prowler/lib/outputs/outputs.py index d600c851a66..2a61e17c364 100644 --- a/prowler/lib/outputs/outputs.py +++ b/prowler/lib/outputs/outputs.py @@ -13,6 +13,8 @@ def stdout_report(finding, color, verbose, status, fix): details = finding.location.lower() if finding.check_metadata.Provider == "kubernetes": details = finding.namespace.lower() + if finding.check_metadata.Provider == "microsoft365": + details = finding.location if (verbose or fix) and (not status or finding.status in status): if finding.muted: diff --git a/prowler/providers/microsoft365/exceptions/exceptions.py b/prowler/providers/microsoft365/exceptions/exceptions.py index 4cbb617a7d2..fb635b00c86 100644 --- a/prowler/providers/microsoft365/exceptions/exceptions.py +++ b/prowler/providers/microsoft365/exceptions/exceptions.py @@ -1,16 +1,16 @@ from prowler.exceptions.exceptions import ProwlerException -# Exceptions codes from 2000 to 2999 are reserved for Microsoft365 exceptions +# Exceptions codes from 5000 to 5999 are reserved for Microsoft365 exceptions class Microsoft365BaseException(ProwlerException): """Base class for Microsoft365 Errors.""" - AZURE_ERROR_CODES = { - (2000, "Microsoft365EnvironmentVariableError"): { + MICROSOFT365_ERROR_CODES = { + (5000, "Microsoft365EnvironmentVariableError"): { "message": "Microsoft365 environment variable error", "remediation": "Check the Microsoft365 environment variables and ensure they are properly set.", }, - (2001, "Microsoft365NoSubscriptionsError"): { + (5001, "Microsoft365NoSubscriptionsError"): { "message": "No Microsoft365 subscriptions found", "remediation": "Check the Microsoft365 subscriptions and ensure they are properly set up.", }, @@ -22,14 +22,6 @@ class Microsoft365BaseException(ProwlerException): "message": "No Microsoft365 authentication method found", "remediation": "Check that any authentication method is properly set up for Microsoft365.", }, - (2004, "Microsoft365BrowserAuthNoTenantIDError"): { - "message": "Microsoft365 browser authentication error: no tenant ID found", - "remediation": "To use browser authentication, ensure the tenant ID is properly set.", - }, - (2005, "Microsoft365TenantIDNoBrowserAuthError"): { - "message": "Microsoft365 tenant ID error: browser authentication not found", - "remediation": "To use browser authentication, both the tenant ID and browser authentication must be properly set.", - }, (2006, "Microsoft365ArgumentTypeValidationError"): { "message": "Microsoft365 argument type validation error", "remediation": "Check the provided argument types specific to Microsoft365 and ensure they meet the required format.", @@ -106,7 +98,7 @@ class Microsoft365BaseException(ProwlerException): def __init__(self, code, file=None, original_exception=None, message=None): provider = "Microsoft365" - error_info = self.AZURE_ERROR_CODES.get((code, self.__class__.__name__)) + error_info = self.MICROSOFT365_ERROR_CODES.get((code, self.__class__.__name__)) if message: error_info["message"] = message super().__init__( diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index 9f424721ef3..b81bce7f11b 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -1,7 +1,9 @@ import asyncio import os +import re from argparse import ArgumentTypeError from os import getenv +from uuid import UUID import requests from azure.core.exceptions import ClientAuthenticationError, HttpResponseError @@ -20,11 +22,17 @@ from prowler.providers.common.provider import Provider from prowler.providers.microsoft365.exceptions.exceptions import ( Microsoft365ArgumentTypeValidationError, + Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError, Microsoft365CredentialsUnavailableError, Microsoft365EnvironmentVariableError, Microsoft365GetTokenIdentityError, Microsoft365HTTPResponseError, + Microsoft365NotValidClientIdError, + Microsoft365NotValidClientSecretError, + Microsoft365NotValidTenantIdError, Microsoft365SetUpRegionConfigError, + Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError, + Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError, ) from prowler.providers.microsoft365.lib.arguments.arguments import ( validate_microsoft365_region, @@ -267,7 +275,7 @@ def print_credentials(self): f"Microsoft365 Identity Type: {Fore.YELLOW}{self._identity.identity_type}{Style.RESET_ALL} Microsoft365 Identity ID: {Fore.YELLOW}{self._identity.identity_id}{Style.RESET_ALL}", ] report_title = ( - f"{Style.BRIGHT}Using the Azure credentials below:{Style.RESET_ALL}" + f"{Style.BRIGHT}Using the Microsoft365 credentials below:{Style.RESET_ALL}" ) print_boxes(report_lines, report_title) @@ -317,7 +325,7 @@ def setup_session( @staticmethod def check_application_creds_env_vars(): """ - Checks the presence of required environment variables for application authentication against Azure. + Checks the presence of required environment variables for application authentication against Microsoft365. This method checks for the presence of the following environment variables: - APP_CLIENT_ID: Microsoft365 client ID @@ -453,3 +461,132 @@ def get_locations(self, credentials) -> dict[str, list[str]]: for location in data["value"]: locations[display_name].append(location["name"]) return locations + + @staticmethod + def validate_static_credentials( + tenant_id: str = None, client_id: str = None, client_secret: str = None + ) -> dict: + """ + Validates the static credentials for the Microsoft365 provider. + + Args: + tenant_id (str): The Microsoft365 Active Directory tenant ID. + client_id (str): The Microsoft365 client ID. + client_secret (str): The Microsoft365 client secret. + + Raises: + Microsoft365NotValidTenantIdError: If the provided Microsoft365 Tenant ID is not valid. + Microsoft365NotValidClientIdError: If the provided Microsoft365 Client ID is not valid. + Microsoft365NotValidClientSecretError: If the provided Microsoft365 Client Secret is not valid. + Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError: If the provided Microsoft365 Client ID and Client Secret do not belong to the specified Tenant ID. + Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError: If the provided Microsoft365 Tenant ID and Client Secret do not belong to the specified Client ID. + Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError: If the provided Microsoft365 Tenant ID and Client ID do not belong to the specified Client Secret. + + Returns: + dict: A dictionary containing the validated static credentials. + """ + # Validate the Tenant ID + try: + UUID(tenant_id) + except ValueError: + raise Microsoft365NotValidTenantIdError( + file=os.path.basename(__file__), + message="The provided Microsoft365 Tenant ID is not valid.", + ) + + # Validate the Client ID + try: + UUID(client_id) + except ValueError: + raise Microsoft365NotValidClientIdError( + file=os.path.basename(__file__), + message="The provided Microsoft365 Client ID is not valid.", + ) + # Validate the Client Secret + if not re.match("^[a-zA-Z0-9._~-]+$", client_secret): + raise Microsoft365NotValidClientSecretError( + file=os.path.basename(__file__), + message="The provided Microsoft365 Client Secret is not valid.", + ) + + try: + Microsoft365Provider.verify_client(tenant_id, client_id, client_secret) + return { + "tenant_id": tenant_id, + "client_id": client_id, + "client_secret": client_secret, + } + except Microsoft365NotValidTenantIdError as tenant_id_error: + logger.error( + f"{tenant_id_error.__class__.__name__}[{tenant_id_error.__traceback__.tb_lineno}]: {tenant_id_error}" + ) + raise Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError( + file=os.path.basename(__file__), + message="The provided Microsoft365 Client ID and Client Secret do not belong to the specified Tenant ID.", + ) + except Microsoft365NotValidClientIdError as client_id_error: + logger.error( + f"{client_id_error.__class__.__name__}[{client_id_error.__traceback__.tb_lineno}]: {client_id_error}" + ) + raise Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError( + file=os.path.basename(__file__), + message="The provided Microsoft365 Tenant ID and Client Secret do not belong to the specified Client ID.", + ) + except Microsoft365NotValidClientSecretError as client_secret_error: + logger.error( + f"{client_secret_error.__class__.__name__}[{client_secret_error.__traceback__.tb_lineno}]: {client_secret_error}" + ) + raise Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError( + file=os.path.basename(__file__), + message="The provided Microsoft365 Tenant ID and Client ID do not belong to the specified Client Secret.", + ) + + @staticmethod + def verify_client(tenant_id, client_id, client_secret) -> None: + """ + Verifies the Microsoft365 client credentials using the specified tenant ID, client ID, and client secret. + + Args: + tenant_id (str): The Microsoft365 Active Directory tenant ID. + client_id (str): The Microsoft365 client ID. + client_secret (str): The Microsoft365 client secret. + + Raises: + Microsoft365NotValidTenantIdError: If the provided Microsoft365 Tenant ID is not valid. + Microsoft365NotValidClientIdError: If the provided Microsoft365 Client ID is not valid. + Microsoft365NotValidClientSecretError: If the provided Microsoft365 Client Secret is not valid. + + Returns: + None + """ + url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token" + headers = { + "Content-Type": "application/x-www-form-urlencoded", + "Accept": "application/json", + } + data = { + "grant_type": "client_credentials", + "client_id": client_id, + "client_secret": client_secret, + "scope": "https://graph.microsoft.com/.default", + } + response = requests.post(url, headers=headers, data=data).json() + if "access_token" not in response.keys() and "error_codes" in response.keys(): + if f"Tenant '{tenant_id}'" in response["error_description"]: + raise Microsoft365NotValidTenantIdError( + file=os.path.basename(__file__), + message="The provided Microsoft 365 Tenant ID is not valid for the specified Client ID and Client Secret.", + ) + if ( + f"Application with identifier '{client_id}'" + in response["error_description"] + ): + raise Microsoft365NotValidClientIdError( + file=os.path.basename(__file__), + message="The provided Microsoft 365 Client ID is not valid for the specified Tenant ID and Client Secret.", + ) + if "Invalid client secret provided" in response["error_description"]: + raise Microsoft365NotValidClientSecretError( + file=os.path.basename(__file__), + message="The provided Microsoft 365 Client Secret is not valid for the specified Tenant ID and Client ID.", + ) diff --git a/prowler/providers/microsoft365/models.py b/prowler/providers/microsoft365/models.py index 30672926188..e17a836cca5 100644 --- a/prowler/providers/microsoft365/models.py +++ b/prowler/providers/microsoft365/models.py @@ -20,13 +20,6 @@ class Microsoft365RegionConfig(BaseModel): credential_scopes: list = [] -class Microsoft365Subscription(BaseModel): - id: str - subscription_id: str - display_name: str - state: str - - class Microsoft365OutputOptions(ProviderOutputOptions): def __init__(self, arguments, bulk_checks_metadata, identity): # First call Provider_Output_Options init From 4bd81deecdfb0983711c8ca4692fbe555400db14 Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Tue, 26 Nov 2024 09:31:28 +0100 Subject: [PATCH 06/44] feat(microsoft365): Rename users service to admincenter and add some new checks following CIS recommendations --- .../{users => admincenter}/__init__.py | 0 .../admincenter/admincenter_client.py | 6 + .../__init__.py | 0 ...groups_not_public_visibility.metadata.json | 33 ++++ ...dmincenter_groups_not_public_visibility.py | 32 ++++ .../__init__.py | 0 ...ns_reduced_license_footprint.metadata.json | 33 ++++ ..._users_admins_reduced_license_footprint.py | 32 ++++ .../admincenter/admincenter_service.py | 156 ++++++++++++++++++ .../__init__.py | 0 ...ns_reduced_license_footprint.metadata.json | 33 ++++ ..._users_admins_reduced_license_footprint.py | 32 ++++ .../__init__.py | 0 ..._two_and_four_global_admins.metadata.json} | 4 +- ...sers_between_two_and_four_global_admins.py | 40 +++++ ...sers_administrative_accounts_cloud_only.py | 37 ----- .../services/users/users_client.py | 4 - .../services/users/users_service.py | 107 ------------ 18 files changed, 399 insertions(+), 150 deletions(-) rename prowler/providers/microsoft365/services/{users => admincenter}/__init__.py (100%) create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_client.py create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/__init__.py create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/__init__.py create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/admincenter_users_admins_reduced_license_footprint.metadata.json create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/admincenter_users_admins_reduced_license_footprint.py create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_service.py create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/__init__.py create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/__init__.py rename prowler/providers/microsoft365/services/{users/users_administrative_accounts_cloud_only/users_administrative_accounts_cloud_only.metadata.json => admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json} (90%) create mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py delete mode 100644 prowler/providers/microsoft365/services/users/users_administrative_accounts_cloud_only/users_administrative_accounts_cloud_only.py delete mode 100644 prowler/providers/microsoft365/services/users/users_client.py delete mode 100644 prowler/providers/microsoft365/services/users/users_service.py diff --git a/prowler/providers/microsoft365/services/users/__init__.py b/prowler/providers/microsoft365/services/admincenter/__init__.py similarity index 100% rename from prowler/providers/microsoft365/services/users/__init__.py rename to prowler/providers/microsoft365/services/admincenter/__init__.py diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_client.py b/prowler/providers/microsoft365/services/admincenter/admincenter_client.py new file mode 100644 index 00000000000..72facdeefd0 --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_client.py @@ -0,0 +1,6 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + AdminCenter, +) + +admincenter_client = AdminCenter(Provider.get_global_provider()) diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/__init__.py b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json new file mode 100644 index 00000000000..6396d341648 --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json @@ -0,0 +1,33 @@ +{ + "Provider": "microsoft365", + "CheckID": "admincenter_users_admins_reduced_license_footprint", + "CheckTitle": "Ensure administrative accounts use licenses with a reduced application footprint", + "CheckType": [ + "Identity", + "LicenseManagement" + ], + "ServiceName": "Microsoft Entra ID", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "AdministrativeAccount", + "Description": "Administrative accounts must use licenses with a reduced application footprint, such as Microsoft Entra ID P1 or P2, or avoid licenses entirely when possible. This minimizes the attack surface associated with privileged identities.", + "Risk": "Licensing administrative accounts with applications like email or collaborative tools increases their exposure to social engineering attacks and malicious content, putting privileged accounts at risk.", + "RelatedUrl": "https://learn.microsoft.com/en-us/azure/active-directory/privileged-identity-management/pim-get-started", + "Remediation": { + "Code": { + "CLI": "Set-MsolUserLicense -UserPrincipalName -RemoveLicenses ", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Assign Microsoft Entra ID P1 or P2 licenses to administrative accounts to participate in essential security services without enabling access to vulnerable applications.", + "Url": "https://learn.microsoft.com/en-us/microsoft-365/security/identity-protection?view=o365-worldwide" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py new file mode 100644 index 00000000000..4ea90cd6eaa --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py @@ -0,0 +1,32 @@ +from prowler.lib.check.models import Check, Check_Report_Microsoft365 +from prowler.providers.microsoft365.services.admincenter.admincenter_client import ( + admincenter_client, +) + + +class admincenter_groups_not_public_visibility(Check): + def execute(self) -> Check_Report_Microsoft365: + findings = [] + allowed_licenses = ["AAD_PREMIUM", "AAD_PREMIUM_P2"] + for tenant_domain, groups in admincenter_client.groups.items(): + for group_id, group in groups.items(): + admin_roles = [ + role + for role in group.directory_roles + if "Administrator" in role or "Globar Reader" in role + ] + + if admin_roles: + report = Check_Report_Microsoft365(self.metadata()) + report.resource_id = group.id + report.resource_name = group.name + report.status = "FAIL" + report.status_extended = f"group {group.name} has administrative roles {admin_roles} and license {group.license}." + + if group.license in allowed_licenses: + report.status = "PASS" + report.status_extended = f"group {group.name} has administrative roles {admin_roles} and a valid license: {group.license}." + + findings.append(report) + + return findings diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/__init__.py b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/admincenter_users_admins_reduced_license_footprint.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/admincenter_users_admins_reduced_license_footprint.metadata.json new file mode 100644 index 00000000000..6396d341648 --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/admincenter_users_admins_reduced_license_footprint.metadata.json @@ -0,0 +1,33 @@ +{ + "Provider": "microsoft365", + "CheckID": "admincenter_users_admins_reduced_license_footprint", + "CheckTitle": "Ensure administrative accounts use licenses with a reduced application footprint", + "CheckType": [ + "Identity", + "LicenseManagement" + ], + "ServiceName": "Microsoft Entra ID", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "AdministrativeAccount", + "Description": "Administrative accounts must use licenses with a reduced application footprint, such as Microsoft Entra ID P1 or P2, or avoid licenses entirely when possible. This minimizes the attack surface associated with privileged identities.", + "Risk": "Licensing administrative accounts with applications like email or collaborative tools increases their exposure to social engineering attacks and malicious content, putting privileged accounts at risk.", + "RelatedUrl": "https://learn.microsoft.com/en-us/azure/active-directory/privileged-identity-management/pim-get-started", + "Remediation": { + "Code": { + "CLI": "Set-MsolUserLicense -UserPrincipalName -RemoveLicenses ", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Assign Microsoft Entra ID P1 or P2 licenses to administrative accounts to participate in essential security services without enabling access to vulnerable applications.", + "Url": "https://learn.microsoft.com/en-us/microsoft-365/security/identity-protection?view=o365-worldwide" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/admincenter_users_admins_reduced_license_footprint.py b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/admincenter_users_admins_reduced_license_footprint.py new file mode 100644 index 00000000000..80420a7e8ca --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/admincenter_users_admins_reduced_license_footprint.py @@ -0,0 +1,32 @@ +from prowler.lib.check.models import Check, Check_Report_Microsoft365 +from prowler.providers.microsoft365.services.admincenter.admincenter_client import ( + admincenter_client, +) + + +class admincenter_users_admins_reduced_license_footprint(Check): + def execute(self) -> Check_Report_Microsoft365: + findings = [] + allowed_licenses = ["AAD_PREMIUM", "AAD_PREMIUM_P2"] + for tenant_domain, users in admincenter_client.users.items(): + for user_principal_name, user in users.items(): + admin_roles = [ + role + for role in user.directory_roles + if "Administrator" in role or "Globar Reader" in role + ] + + if admin_roles: + report = Check_Report_Microsoft365(self.metadata()) + report.resource_id = user.id + report.resource_name = user.name + report.status = "FAIL" + report.status_extended = f"User {user.name} has administrative roles {admin_roles} and license {user.license}." + + if user.license in allowed_licenses: + report.status = "PASS" + report.status_extended = f"User {user.name} has administrative roles {admin_roles} and a valid license: {user.license}." + + findings.append(report) + + return findings diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_service.py b/prowler/providers/microsoft365/services/admincenter/admincenter_service.py new file mode 100644 index 00000000000..7a77475190b --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_service.py @@ -0,0 +1,156 @@ +from asyncio import gather, get_event_loop +from typing import List, Optional + +from msgraph.generated.models.o_data_errors.o_data_error import ODataError +from pydantic import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.microsoft365.lib.service.service import Microsoft365Service +from prowler.providers.microsoft365.microsoft365_provider import Microsoft365Provider + + +class AdminCenter(Microsoft365Service): + def __init__(self, provider: Microsoft365Provider): + super().__init__(provider) + + loop = get_event_loop() + + # Get users first alone because it is a dependency for other attributes + self.users = loop.run_until_complete(self._get_users()) + + attributes = loop.run_until_complete( + gather( + self._get_directory_roles(), + self._get_groups(), + ) + ) + + self.directory_roles = attributes[0] + self.groups = attributes[1] + + async def _get_users(self): + logger.info("Microsoft365 - Getting users...") + users = {} + try: + for tenant, client in self.clients.items(): + users_list = await client.users.get() + users.update({tenant: {}}) + for user in users_list.value: + license_details = await client.users.by_user_id( + user.id + ).license_details.get() + try: + mailbox_settings = await client.users.by_user_id( + user.id + ).mailbox_settings.get() + user_type = mailbox_settings.user_purpose + except ODataError as error: + if error.error.code == "MailboxNotEnabledForRESTAPI": + user_type = "inactive" + else: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + users[tenant].update( + { + user.id: User( + id=user.id, + name=user.display_name, + on_premises_sync_enabled=user.on_premises_sync_enabled, + license=( + license_details.value[0].sku_part_number + if license_details.value + else None + ), + user_type=user_type, + account_enabled=user.account_enabled, + ) + } + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + return users + + async def _get_directory_roles(self): + logger.info("Microsoft365 - Getting directory roles...") + directory_roles_with_members = {} + try: + for tenant, client in self.clients.items(): + directory_roles_with_members.update({tenant: {}}) + directory_roles = await client.directory_roles.get() + for directory_role in directory_roles.value: + directory_role_members = ( + await client.directory_roles.by_directory_role_id( + directory_role.id + ).members.get() + ) + members_with_roles = [] + for member in directory_role_members.value: + user = self.users[tenant].get(member.user_principal_name, None) + if user: + user.directory_roles.append(directory_role.display_name) + members_with_roles.append(user) + + directory_roles_with_members[tenant].update( + { + directory_role.id: DirectoryRole( + id=directory_role.id, + name=directory_role.display_name, + members=members_with_roles, + ) + } + ) + + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return directory_roles_with_members + + async def _get_groups(self): + logger.info("Microsoft365 - Getting groups...") + groups = {} + try: + for tenant, client in self.clients.items(): + groups_list = await client.groups.get() + groups.update((tenant, {})) + for group in groups_list.value: + groups[tenant].update( + { + group.id: Group( + id=group.id, + name=group.display_name, + visibility=group.visibility, + ) + } + ) + + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return groups + + +class User(BaseModel): + id: str + name: str + directory_roles: List[str] = [] + license: Optional[str] = None + user_type: Optional[str] = None + account_enabled: Optional[bool] = None + + +class DirectoryRole(BaseModel): + id: str + name: str + members: List[User] + + +class Group(BaseModel): + id: str + name: str + visibility: str diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/__init__.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json new file mode 100644 index 00000000000..6396d341648 --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json @@ -0,0 +1,33 @@ +{ + "Provider": "microsoft365", + "CheckID": "admincenter_users_admins_reduced_license_footprint", + "CheckTitle": "Ensure administrative accounts use licenses with a reduced application footprint", + "CheckType": [ + "Identity", + "LicenseManagement" + ], + "ServiceName": "Microsoft Entra ID", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "AdministrativeAccount", + "Description": "Administrative accounts must use licenses with a reduced application footprint, such as Microsoft Entra ID P1 or P2, or avoid licenses entirely when possible. This minimizes the attack surface associated with privileged identities.", + "Risk": "Licensing administrative accounts with applications like email or collaborative tools increases their exposure to social engineering attacks and malicious content, putting privileged accounts at risk.", + "RelatedUrl": "https://learn.microsoft.com/en-us/azure/active-directory/privileged-identity-management/pim-get-started", + "Remediation": { + "Code": { + "CLI": "Set-MsolUserLicense -UserPrincipalName -RemoveLicenses ", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Assign Microsoft Entra ID P1 or P2 licenses to administrative accounts to participate in essential security services without enabling access to vulnerable applications.", + "Url": "https://learn.microsoft.com/en-us/microsoft-365/security/identity-protection?view=o365-worldwide" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py new file mode 100644 index 00000000000..80420a7e8ca --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py @@ -0,0 +1,32 @@ +from prowler.lib.check.models import Check, Check_Report_Microsoft365 +from prowler.providers.microsoft365.services.admincenter.admincenter_client import ( + admincenter_client, +) + + +class admincenter_users_admins_reduced_license_footprint(Check): + def execute(self) -> Check_Report_Microsoft365: + findings = [] + allowed_licenses = ["AAD_PREMIUM", "AAD_PREMIUM_P2"] + for tenant_domain, users in admincenter_client.users.items(): + for user_principal_name, user in users.items(): + admin_roles = [ + role + for role in user.directory_roles + if "Administrator" in role or "Globar Reader" in role + ] + + if admin_roles: + report = Check_Report_Microsoft365(self.metadata()) + report.resource_id = user.id + report.resource_name = user.name + report.status = "FAIL" + report.status_extended = f"User {user.name} has administrative roles {admin_roles} and license {user.license}." + + if user.license in allowed_licenses: + report.status = "PASS" + report.status_extended = f"User {user.name} has administrative roles {admin_roles} and a valid license: {user.license}." + + findings.append(report) + + return findings diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/__init__.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/microsoft365/services/users/users_administrative_accounts_cloud_only/users_administrative_accounts_cloud_only.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json similarity index 90% rename from prowler/providers/microsoft365/services/users/users_administrative_accounts_cloud_only/users_administrative_accounts_cloud_only.metadata.json rename to prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json index 6a6d0b02022..178306390ec 100644 --- a/prowler/providers/microsoft365/services/users/users_administrative_accounts_cloud_only/users_administrative_accounts_cloud_only.metadata.json +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json @@ -1,6 +1,6 @@ { "Provider": "microsoft365", - "CheckID": "users_administrative_accounts_cloud_only", + "CheckID": "admincenter_users_between_two_and_four_global_admins", "CheckTitle": "Ensure Administrative accounts are cloud-only", "CheckType": [], "ServiceName": "users", @@ -13,7 +13,7 @@ "RelatedUrl": "https://learn.microsoft.com/en-us/microsoft-365/security/identity-protection?view=o365-worldwide", "Remediation": { "Code": { - "CLI": "Get-MsolUser -Admin | Where-Object {$_.ImmutableId -ne $null} | Remove-MsolUser", + "CLI": "", "NativeIaC": "", "Other": "", "Terraform": "" diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py new file mode 100644 index 00000000000..e20bfa5216a --- /dev/null +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py @@ -0,0 +1,40 @@ +from prowler.lib.check.models import Check, Check_Report_Microsoft365 +from prowler.providers.microsoft365.services.admincenter.admincenter_client import ( + admincenter_client, +) + + +class admincenter_users_between_two_and_four_global_admins(Check): + def execute(self) -> Check_Report_Microsoft365: + findings = [] + + for ( + tenant_domain, + directory_roles, + ) in admincenter_client.directory_roles.items(): + report = Check_Report_Microsoft365(self.metadata()) + report.status = "FAIL" + report.resource_name = "Global Administrator" + + if "Global Administrator" in directory_roles: + report.resource_id = getattr( + directory_roles["Global Administrator"], + "id", + "Global Administrator", + ) + + num_global_admins = len( + getattr(directory_roles["Global Administrator"], "members", []) + ) + + if num_global_admins >= 2 and num_global_admins < 5: + report.status = "PASS" + report.status_extended = ( + f"There are {num_global_admins} global administrators." + ) + else: + report.status_extended = f"There are {num_global_admins} global administrators. It should be less more than two and less than five." + + findings.append(report) + + return findings diff --git a/prowler/providers/microsoft365/services/users/users_administrative_accounts_cloud_only/users_administrative_accounts_cloud_only.py b/prowler/providers/microsoft365/services/users/users_administrative_accounts_cloud_only/users_administrative_accounts_cloud_only.py deleted file mode 100644 index ea21206aedc..00000000000 --- a/prowler/providers/microsoft365/services/users/users_administrative_accounts_cloud_only/users_administrative_accounts_cloud_only.py +++ /dev/null @@ -1,37 +0,0 @@ -from prowler.lib.check.models import Check, Check_Report_Microsoft365 -from prowler.providers.microsoft365.services.users.users_client import users_client - - -class users_administrative_accounts_cloud_only(Check): - def execute(self) -> Check_Report_Microsoft365: - findings = [] - - for tenant_domain, directory_roles in users_client.directory_roles.items(): - for role_name, directory_role in directory_roles.items(): - report = Check_Report_Microsoft365(self.metadata()) - report.subscription = f"Tenant: {tenant_domain}" - report.resource_name = role_name - report.resource_id = directory_role.id - report.status = "PASS" - - non_compliant_members = [ - member - for member in directory_roles.members - if member.on_premises_sync_enabled - ] - - if non_compliant_members: - report.status = "FAIL" - report.status_extended = ( - f"The following administrators in role '{role_name}' " - f"are synchronized with on-premises: " - f"{', '.join([member.name for member in non_compliant_members])}." - ) - else: - report.status_extended = ( - f"All administrators in role '{role_name}' are cloud-only." - ) - - findings.append(report) - - return findings diff --git a/prowler/providers/microsoft365/services/users/users_client.py b/prowler/providers/microsoft365/services/users/users_client.py deleted file mode 100644 index cc9cd821a43..00000000000 --- a/prowler/providers/microsoft365/services/users/users_client.py +++ /dev/null @@ -1,4 +0,0 @@ -from prowler.providers.common.provider import Provider -from prowler.providers.microsoft365.services.users.users_service import Users - -users_client = Users(Provider.get_global_provider()) diff --git a/prowler/providers/microsoft365/services/users/users_service.py b/prowler/providers/microsoft365/services/users/users_service.py deleted file mode 100644 index 25d2c019472..00000000000 --- a/prowler/providers/microsoft365/services/users/users_service.py +++ /dev/null @@ -1,107 +0,0 @@ -from asyncio import gather, get_event_loop -from typing import List, Optional - -from pydantic import BaseModel - -from prowler.lib.logger import logger -from prowler.providers.microsoft365.lib.service.service import Microsoft365Service -from prowler.providers.microsoft365.microsoft365_provider import Microsoft365Provider - - -class Users(Microsoft365Service): - def __init__(self, provider: Microsoft365Provider): - super().__init__(provider) - - loop = get_event_loop() - - # Get users first alone because it is a dependency for other attributes - self.users = loop.run_until_complete(self._get_users()) - - attributes = loop.run_until_complete( - gather( - self._get_directory_roles(), - ) - ) - - self.directory_roles = attributes[0] - - async def _get_users(self): - logger.info("Entra - Getting users...") - users = {} - try: - for tenant, client in self.clients.items(): - users_list = await client.users.get( - params={ - "$select": "id,displayName,userPrincipalName,onPremisesSyncEnabled" - } - ) - users.update({tenant: {}}) - for user in users_list.value: - users[tenant].update( - { - user.user_principal_name: User( - id=user.id, - name=user.display_name, - on_premises_sync_enabled=user.on_premises_sync_enabled, - ) - } - ) - except Exception as error: - if ( - error.__class__.__name__ == "ODataError" - and error.__dict__.get("response_status_code", None) == 403 - ): - logger.error( - "You need 'UserAuthenticationMethod.Read.All' permission to access this information. It only can be granted through Service Principal authentication." - ) - else: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" - ) - - return users - - async def _get_directory_roles(self): - logger.info("Entra - Getting directory roles...") - directory_roles_with_members = {} - try: - for tenant, client in self.clients.items(): - directory_roles_with_members.update({tenant: {}}) - directory_roles = await client.directory_roles.get() - for directory_role in directory_roles.value: - directory_role_members = ( - await client.directory_roles.by_directory_role_id( - directory_role.id - ).members.get() - ) - directory_roles_with_members[tenant].update( - { - directory_role.display_name: DirectoryRole( - id=directory_role.id, - members=[ - self.users[tenant][member.user_principal_name] - for member in directory_role_members.value - if self.users[tenant].get( - member.user_principal_name, None - ) - ], - ) - } - ) - - except Exception as error: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" - ) - return directory_roles_with_members - - -class User(BaseModel): - id: str - name: str - on_premises_sync_enabled: Optional[bool] = None - - -class DirectoryRole(BaseModel): - id: str - members: List[User] From 4564f0df282075a8dacbdc52de10cedb426fb23d Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Tue, 26 Nov 2024 20:16:24 +0100 Subject: [PATCH 07/44] feat(admincenter): Finish checks and service, add metadata but still has work on it --- ...groups_not_public_visibility.metadata.json | 25 +++++++------- ...dmincenter_groups_not_public_visibility.py | 29 +++++++--------- .../__init__.py | 0 ...ns_reduced_license_footprint.metadata.json | 33 ------------------- ..._users_admins_reduced_license_footprint.py | 32 ------------------ .../admincenter/admincenter_service.py | 9 ++--- ...ns_reduced_license_footprint.metadata.json | 8 ++--- ..._users_admins_reduced_license_footprint.py | 1 + ...n_two_and_four_global_admins.metadata.json | 23 +++++++------ ...sers_between_two_and_four_global_admins.py | 3 +- 10 files changed, 47 insertions(+), 116 deletions(-) delete mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/__init__.py delete mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/admincenter_users_admins_reduced_license_footprint.metadata.json delete mode 100644 prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/admincenter_users_admins_reduced_license_footprint.py diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json index 6396d341648..32131e6cdc9 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json @@ -1,29 +1,28 @@ { "Provider": "microsoft365", - "CheckID": "admincenter_users_admins_reduced_license_footprint", - "CheckTitle": "Ensure administrative accounts use licenses with a reduced application footprint", + "CheckID": "admincenter_groups_not_public_visibility", + "CheckTitle": "Ensure that only organizationally managed/approved public groups exist", "CheckType": [ - "Identity", - "LicenseManagement" + "Users" ], - "ServiceName": "Microsoft Entra ID", + "ServiceName": "Admin Center", "SubServiceName": "", "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "AdministrativeAccount", - "Description": "Administrative accounts must use licenses with a reduced application footprint, such as Microsoft Entra ID P1 or P2, or avoid licenses entirely when possible. This minimizes the attack surface associated with privileged identities.", - "Risk": "Licensing administrative accounts with applications like email or collaborative tools increases their exposure to social engineering attacks and malicious content, putting privileged accounts at risk.", - "RelatedUrl": "https://learn.microsoft.com/en-us/azure/active-directory/privileged-identity-management/pim-get-started", + "Severity": "high", + "ResourceType": "Microsoft365Group", + "Description": "Ensure that only organizationally managed and approved public groups exist to prevent unauthorized access to sensitive group resources like SharePoint, Teams, or other shared assets.", + "Risk": "Unmanaged public groups can allow unauthorized access to organizational resources, posing a risk of data leakage or misuse through easily guessable SharePoint URLs or self-adding to groups via the Azure portal.", + "RelatedUrl": "https://learn.microsoft.com/en-us/microsoft-365/admin/create-groups/manage-groups?view=o365-worldwide", "Remediation": { "Code": { - "CLI": "Set-MsolUserLicense -UserPrincipalName -RemoveLicenses ", + "CLI": "", "NativeIaC": "", "Other": "", "Terraform": "" }, "Recommendation": { - "Text": "Assign Microsoft Entra ID P1 or P2 licenses to administrative accounts to participate in essential security services without enabling access to vulnerable applications.", - "Url": "https://learn.microsoft.com/en-us/microsoft-365/security/identity-protection?view=o365-worldwide" + "Text": "Review and adjust the privacy settings of Microsoft 365 Groups to ensure only organizationally managed and approved public groups exist.", + "Url": "https://learn.microsoft.com/en-us/microsoft-365/security/office-365-security/microsoft-365-groups-governance" } }, "Categories": [], diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py index 4ea90cd6eaa..c5a5ae95201 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py @@ -7,26 +7,21 @@ class admincenter_groups_not_public_visibility(Check): def execute(self) -> Check_Report_Microsoft365: findings = [] - allowed_licenses = ["AAD_PREMIUM", "AAD_PREMIUM_P2"] for tenant_domain, groups in admincenter_client.groups.items(): for group_id, group in groups.items(): - admin_roles = [ - role - for role in group.directory_roles - if "Administrator" in role or "Globar Reader" in role - ] + report = Check_Report_Microsoft365(self.metadata()) + report.resource_id = group.id + report.resource_name = group.name + report.tenant_id = tenant_domain + report.status = "FAIL" + report.status_extended = f"Group {group.name} has {group.visibility} visibility and should be Private." - if admin_roles: - report = Check_Report_Microsoft365(self.metadata()) - report.resource_id = group.id - report.resource_name = group.name - report.status = "FAIL" - report.status_extended = f"group {group.name} has administrative roles {admin_roles} and license {group.license}." + if group.visibility != "Public": + report.status = "PASS" + report.status_extended = ( + f"Group {group.name} has {group.visibility} visibility." + ) - if group.license in allowed_licenses: - report.status = "PASS" - report.status_extended = f"group {group.name} has administrative roles {admin_roles} and a valid license: {group.license}." - - findings.append(report) + findings.append(report) return findings diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/__init__.py b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/admincenter_users_admins_reduced_license_footprint.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/admincenter_users_admins_reduced_license_footprint.metadata.json deleted file mode 100644 index 6396d341648..00000000000 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/admincenter_users_admins_reduced_license_footprint.metadata.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "Provider": "microsoft365", - "CheckID": "admincenter_users_admins_reduced_license_footprint", - "CheckTitle": "Ensure administrative accounts use licenses with a reduced application footprint", - "CheckType": [ - "Identity", - "LicenseManagement" - ], - "ServiceName": "Microsoft Entra ID", - "SubServiceName": "", - "ResourceIdTemplate": "", - "Severity": "medium", - "ResourceType": "AdministrativeAccount", - "Description": "Administrative accounts must use licenses with a reduced application footprint, such as Microsoft Entra ID P1 or P2, or avoid licenses entirely when possible. This minimizes the attack surface associated with privileged identities.", - "Risk": "Licensing administrative accounts with applications like email or collaborative tools increases their exposure to social engineering attacks and malicious content, putting privileged accounts at risk.", - "RelatedUrl": "https://learn.microsoft.com/en-us/azure/active-directory/privileged-identity-management/pim-get-started", - "Remediation": { - "Code": { - "CLI": "Set-MsolUserLicense -UserPrincipalName -RemoveLicenses ", - "NativeIaC": "", - "Other": "", - "Terraform": "" - }, - "Recommendation": { - "Text": "Assign Microsoft Entra ID P1 or P2 licenses to administrative accounts to participate in essential security services without enabling access to vulnerable applications.", - "Url": "https://learn.microsoft.com/en-us/microsoft-365/security/identity-protection?view=o365-worldwide" - } - }, - "Categories": [], - "DependsOn": [], - "RelatedTo": [], - "Notes": "" -} diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/admincenter_users_admins_reduced_license_footprint.py b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/admincenter_users_admins_reduced_license_footprint.py deleted file mode 100644 index 80420a7e8ca..00000000000 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_shared_mailboxes_blocked_sign_in/admincenter_users_admins_reduced_license_footprint.py +++ /dev/null @@ -1,32 +0,0 @@ -from prowler.lib.check.models import Check, Check_Report_Microsoft365 -from prowler.providers.microsoft365.services.admincenter.admincenter_client import ( - admincenter_client, -) - - -class admincenter_users_admins_reduced_license_footprint(Check): - def execute(self) -> Check_Report_Microsoft365: - findings = [] - allowed_licenses = ["AAD_PREMIUM", "AAD_PREMIUM_P2"] - for tenant_domain, users in admincenter_client.users.items(): - for user_principal_name, user in users.items(): - admin_roles = [ - role - for role in user.directory_roles - if "Administrator" in role or "Globar Reader" in role - ] - - if admin_roles: - report = Check_Report_Microsoft365(self.metadata()) - report.resource_id = user.id - report.resource_name = user.name - report.status = "FAIL" - report.status_extended = f"User {user.name} has administrative roles {admin_roles} and license {user.license}." - - if user.license in allowed_licenses: - report.status = "PASS" - report.status_extended = f"User {user.name} has administrative roles {admin_roles} and a valid license: {user.license}." - - findings.append(report) - - return findings diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_service.py b/prowler/providers/microsoft365/services/admincenter/admincenter_service.py index 7a77475190b..d63dc2c70f0 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_service.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_service.py @@ -56,14 +56,12 @@ async def _get_users(self): user.id: User( id=user.id, name=user.display_name, - on_premises_sync_enabled=user.on_premises_sync_enabled, license=( license_details.value[0].sku_part_number if license_details.value else None ), user_type=user_type, - account_enabled=user.account_enabled, ) } ) @@ -89,14 +87,14 @@ async def _get_directory_roles(self): ) members_with_roles = [] for member in directory_role_members.value: - user = self.users[tenant].get(member.user_principal_name, None) + user = self.users[tenant].get(member.id, None) if user: user.directory_roles.append(directory_role.display_name) members_with_roles.append(user) directory_roles_with_members[tenant].update( { - directory_role.id: DirectoryRole( + directory_role.display_name: DirectoryRole( id=directory_role.id, name=directory_role.display_name, members=members_with_roles, @@ -116,7 +114,7 @@ async def _get_groups(self): try: for tenant, client in self.clients.items(): groups_list = await client.groups.get() - groups.update((tenant, {})) + groups.update({tenant: {}}) for group in groups_list.value: groups[tenant].update( { @@ -141,7 +139,6 @@ class User(BaseModel): directory_roles: List[str] = [] license: Optional[str] = None user_type: Optional[str] = None - account_enabled: Optional[bool] = None class DirectoryRole(BaseModel): diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json index 6396d341648..a4b1d8db439 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json @@ -6,24 +6,24 @@ "Identity", "LicenseManagement" ], - "ServiceName": "Microsoft Entra ID", + "ServiceName": "Admin Center", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", "ResourceType": "AdministrativeAccount", "Description": "Administrative accounts must use licenses with a reduced application footprint, such as Microsoft Entra ID P1 or P2, or avoid licenses entirely when possible. This minimizes the attack surface associated with privileged identities.", "Risk": "Licensing administrative accounts with applications like email or collaborative tools increases their exposure to social engineering attacks and malicious content, putting privileged accounts at risk.", - "RelatedUrl": "https://learn.microsoft.com/en-us/azure/active-directory/privileged-identity-management/pim-get-started", + "RelatedUrl": "https://learn.microsoft.com/en-us/microsoft-365/enterprise/protect-your-global-administrator-accounts?view=o365-worldwide", "Remediation": { "Code": { - "CLI": "Set-MsolUserLicense -UserPrincipalName -RemoveLicenses ", + "CLI": "", "NativeIaC": "", "Other": "", "Terraform": "" }, "Recommendation": { "Text": "Assign Microsoft Entra ID P1 or P2 licenses to administrative accounts to participate in essential security services without enabling access to vulnerable applications.", - "Url": "https://learn.microsoft.com/en-us/microsoft-365/security/identity-protection?view=o365-worldwide" + "Url": "https://learn.microsoft.com/en-us/microsoft-365/admin/add-users/add-users?view=o365-worldwide" } }, "Categories": [], diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py index 80420a7e8ca..53756891024 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py @@ -20,6 +20,7 @@ def execute(self) -> Check_Report_Microsoft365: report = Check_Report_Microsoft365(self.metadata()) report.resource_id = user.id report.resource_name = user.name + report.tenant_id = tenant_domain report.status = "FAIL" report.status_extended = f"User {user.name} has administrative roles {admin_roles} and license {user.license}." diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json index 178306390ec..57e5ee139b0 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json @@ -1,16 +1,16 @@ { "Provider": "microsoft365", "CheckID": "admincenter_users_between_two_and_four_global_admins", - "CheckTitle": "Ensure Administrative accounts are cloud-only", + "CheckTitle": "Ensure that between two and four global admins are designated", "CheckType": [], - "ServiceName": "users", + "ServiceName": "Admin Center", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AdministrativeAccount", - "Description": "Administrative accounts must be cloud-only and separated from on-premises accounts. These accounts should not have applications assigned to them and should be used exclusively for administrative tasks.", - "Risk": "Failing to separate administrative accounts can lead to compromised security in hybrid environments. A breach in the cloud could potentially impact the on-premises environment and vice versa.", - "RelatedUrl": "https://learn.microsoft.com/en-us/microsoft-365/security/identity-protection?view=o365-worldwide", + "ResourceType": "AdministrativeRole", + "Description": "Ensure that there are between two and four global administrators designated in your tenant. This ensures monitoring, redundancy, and reduces the risk associated with having too many privileged accounts.", + "Risk": "Having only one global administrator increases the risk of unmonitored actions and operational disruptions if that administrator is unavailable. Having more than four increases the likelihood of a breach through one of these highly privileged accounts.", + "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/best-practices#5-limit-the-number-of-global-administrators-to-less-than-5", "Remediation": { "Code": { "CLI": "", @@ -19,12 +19,15 @@ "Terraform": "" }, "Recommendation": { - "Text": "Create cloud-only administrative accounts and ensure they are not synchronized from on-premises directories. Remove any unnecessary application assignments.", - "Url": "https://learn.microsoft.com/en-us/azure/active-directory/roles/security-design-administrative-accounts" + "Text": "Review the number of global administrators in your tenant. Add or remove global admins as necessary to ensure compliance with the recommended range of two to four.", + "Url": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/manage-roles-portal" } }, - "Categories": [], + "Categories": [ + "identity-management", + "privilege-management" + ], "DependsOn": [], "RelatedTo": [], - "Notes": "Administrative accounts should be strictly cloud-only and dedicated to admin tasks. Migrate all necessary permissions, including M365 and Azure RBAC roles, to these accounts." + "Notes": "" } diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py index e20bfa5216a..e6186605557 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py @@ -14,6 +14,7 @@ def execute(self) -> Check_Report_Microsoft365: ) in admincenter_client.directory_roles.items(): report = Check_Report_Microsoft365(self.metadata()) report.status = "FAIL" + report.tenant_id = tenant_domain report.resource_name = "Global Administrator" if "Global Administrator" in directory_roles: @@ -33,7 +34,7 @@ def execute(self) -> Check_Report_Microsoft365: f"There are {num_global_admins} global administrators." ) else: - report.status_extended = f"There are {num_global_admins} global administrators. It should be less more than two and less than five." + report.status_extended = f"There are {num_global_admins} global administrators. It should be more than two and less than five." findings.append(report) From eb4e41b55755f4867aa9597ebdeda5dcbdcdc87c Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Wed, 27 Nov 2024 06:53:50 +0100 Subject: [PATCH 08/44] fix(microsoft365): Adding verify connection to m365 provider, currently not working --- prowler/providers/common/provider.py | 1 - .../microsoft365/exceptions/exceptions.py | 44 +- .../microsoft365/lib/service/service.py | 2 +- .../microsoft365/microsoft365_provider.py | 393 ++++++++++++------ prowler/providers/microsoft365/models.py | 8 +- 5 files changed, 286 insertions(+), 162 deletions(-) diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index 88bf32faa89..9abac8ff9a7 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -213,7 +213,6 @@ def init_global_provider(arguments: Namespace) -> None: ) elif "microsoft365" in provider_class_name.lower(): provider_class( - app_env_auth=arguments.app_env_auth, config_path=arguments.config_file, mutelist_path=arguments.mutelist_file, fixer_config=fixer_config, diff --git a/prowler/providers/microsoft365/exceptions/exceptions.py b/prowler/providers/microsoft365/exceptions/exceptions.py index fb635b00c86..cac4ca4729b 100644 --- a/prowler/providers/microsoft365/exceptions/exceptions.py +++ b/prowler/providers/microsoft365/exceptions/exceptions.py @@ -6,91 +6,91 @@ class Microsoft365BaseException(ProwlerException): """Base class for Microsoft365 Errors.""" MICROSOFT365_ERROR_CODES = { - (5000, "Microsoft365EnvironmentVariableError"): { + (6000, "Microsoft365EnvironmentVariableError"): { "message": "Microsoft365 environment variable error", "remediation": "Check the Microsoft365 environment variables and ensure they are properly set.", }, - (5001, "Microsoft365NoSubscriptionsError"): { + (6001, "Microsoft365NoSubscriptionsError"): { "message": "No Microsoft365 subscriptions found", "remediation": "Check the Microsoft365 subscriptions and ensure they are properly set up.", }, - (2002, "Microsoft365SetUpIdentityError"): { + (6002, "Microsoft365SetUpIdentityError"): { "message": "Microsoft365 identity setup error related with credentials", "remediation": "Check credentials and ensure they are properly set up for Microsoft365 and the identity provider.", }, - (2003, "Microsoft365NoAuthenticationMethodError"): { + (6003, "Microsoft365NoAuthenticationMethodError"): { "message": "No Microsoft365 authentication method found", "remediation": "Check that any authentication method is properly set up for Microsoft365.", }, - (2006, "Microsoft365ArgumentTypeValidationError"): { + (6006, "Microsoft365ArgumentTypeValidationError"): { "message": "Microsoft365 argument type validation error", "remediation": "Check the provided argument types specific to Microsoft365 and ensure they meet the required format.", }, - (2007, "Microsoft365SetUpRegionConfigError"): { + (6007, "Microsoft365SetUpRegionConfigError"): { "message": "Microsoft365 region configuration setup error", "remediation": "Check the Microsoft365 region configuration and ensure it is properly set up.", }, - (2008, "Microsoft365DefaultMicrosoft365CredentialError"): { + (6008, "Microsoft365DefaultMicrosoft365CredentialError"): { "message": "Error in DefaultMicrosoft365Credential", "remediation": "Check that all the attributes are properly set up for the DefaultMicrosoft365Credential.", }, - (2009, "Microsoft365InteractiveBrowserCredentialError"): { + (6009, "Microsoft365InteractiveBrowserCredentialError"): { "message": "Error retrieving InteractiveBrowserCredential", "remediation": "Check your browser and ensure that the tenant ID and browser authentication are properly set.", }, - (2010, "Microsoft365HTTPResponseError"): { + (6010, "Microsoft365HTTPResponseError"): { "message": "Error in HTTP response from Microsoft365", "remediation": "", }, - (2011, "Microsoft365CredentialsUnavailableError"): { + (6011, "Microsoft365CredentialsUnavailableError"): { "message": "Error trying to configure Microsoft365 credentials because they are unavailable", "remediation": "Check the dictionary and ensure it is properly set up for Microsoft365 credentials. TENANT_ID, CLIENT_ID and CLIENT_SECRET are required.", }, - (2012, "Microsoft365GetTokenIdentityError"): { + (6012, "Microsoft365GetTokenIdentityError"): { "message": "Error trying to get token from Microsoft365 Identity", "remediation": "Check the Microsoft365 Identity and ensure it is properly set up.", }, - (2013, "Microsoft365NotTenantIdButClientIdAndClienSecretError"): { + (6013, "Microsoft365NotTenantIdButClientIdAndClienSecretError"): { "message": "The provided credentials are not a tenant ID but a client ID and client secret", "remediation": "Tenant Id, Client Id and Client Secret are required for Microsoft365 credentials. Make sure you are using the correct credentials.", }, - (2014, "Microsoft365ClientAuthenticationError"): { + (6014, "Microsoft365ClientAuthenticationError"): { "message": "Error in client authentication", "remediation": "Check the client authentication and ensure it is properly set up.", }, - (2015, "Microsoft365SetUpSessionError"): { + (6015, "Microsoft365SetUpSessionError"): { "message": "Error setting up session", "remediation": "Check the session setup and ensure it is properly set up.", }, - (2016, "Microsoft365NotValidTenantIdError"): { + (6016, "Microsoft365NotValidTenantIdError"): { "message": "The provided tenant ID is not valid", "remediation": "Check the tenant ID and ensure it is a valid ID.", }, - (2017, "Microsoft365NotValidClientIdError"): { + (6017, "Microsoft365NotValidClientIdError"): { "message": "The provided client ID is not valid", "remediation": "Check the client ID and ensure it is a valid ID.", }, - (2018, "Microsoft365NotValidClientSecretError"): { + (6018, "Microsoft365NotValidClientSecretError"): { "message": "The provided client secret is not valid", "remediation": "Check the client secret and ensure it is a valid secret.", }, - (2019, "Microsoft365ConfigCredentialsError"): { + (6019, "Microsoft365ConfigCredentialsError"): { "message": "Error in configuration of Microsoft365 credentials", "remediation": "Check the configuration of Microsoft365 credentials and ensure it is properly set up.", }, - (2020, "Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError"): { + (6020, "Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError"): { "message": "The provided client ID and client secret do not belong to the provided tenant ID", "remediation": "Check the client ID and client secret and ensure they belong to the provided tenant ID.", }, - (2021, "Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError"): { + (6021, "Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError"): { "message": "The provided tenant ID and client secret do not belong to the provided client ID", "remediation": "Check the tenant ID and client secret and ensure they belong to the provided client ID.", }, - (2022, "Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError"): { + (6022, "Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError"): { "message": "The provided tenant ID and client ID do not belong to the provided client secret", "remediation": "Check the tenant ID and client ID and ensure they belong to the provided client secret.", }, - (2023, "Microsoft365InvalidProviderIdError"): { + (6023, "Microsoft365InvalidProviderIdError"): { "message": "The provided provider_id does not match with the available subscriptions", "remediation": "Check the provider_id and ensure it is a valid subscription for the given credentials.", }, diff --git a/prowler/providers/microsoft365/lib/service/service.py b/prowler/providers/microsoft365/lib/service/service.py index 2f4b7269505..4863a708a00 100644 --- a/prowler/providers/microsoft365/lib/service/service.py +++ b/prowler/providers/microsoft365/lib/service/service.py @@ -15,7 +15,7 @@ def __init__( provider.region_config, ) - self.locations = provider.locations + # self.locations = provider.locations self.audit_config = provider.audit_config self.fixer_config = provider.fixer_config diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index b81bce7f11b..f4882701411 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -18,15 +18,20 @@ ) from prowler.lib.logger import logger from prowler.lib.utils.utils import print_boxes -from prowler.providers.common.models import Audit_Metadata +from prowler.providers.common.models import Audit_Metadata, Connection from prowler.providers.common.provider import Provider from prowler.providers.microsoft365.exceptions.exceptions import ( Microsoft365ArgumentTypeValidationError, + Microsoft365ClientAuthenticationError, Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError, + Microsoft365ConfigCredentialsError, Microsoft365CredentialsUnavailableError, + Microsoft365DefaultMicrosoft365CredentialError, Microsoft365EnvironmentVariableError, Microsoft365GetTokenIdentityError, Microsoft365HTTPResponseError, + Microsoft365InteractiveBrowserCredentialError, + Microsoft365InvalidProviderIdError, Microsoft365NotValidClientIdError, Microsoft365NotValidClientSecretError, Microsoft365NotValidTenantIdError, @@ -59,7 +64,6 @@ class Microsoft365Provider(Provider): _identity (Microsoft365IdentityInfo): The identity information for the Microsoft365 provider. _audit_config (dict): The audit configuration for the Microsoft365 provider. _region_config (Microsoft365RegionConfig): The region configuration for the Microsoft365 provider. - _locations (dict): A dictionary containing the available locations for the Microsoft365 provider. _mutelist (Microsoft365Mutelist): The mutelist object associated with the Microsoft365 provider. audit_metadata (Audit_Metadata): The audit metadata for the Microsoft365 provider. @@ -69,12 +73,10 @@ class Microsoft365Provider(Provider): type(self): Returns the type of the Microsoft365 provider. session(self): Returns the session object associated with the Microsoft365 provider. region_config(self): Returns the region configuration for the Microsoft365 provider. - locations(self): Returns a list of available locations for the Microsoft365 provider. audit_config(self): Returns the audit configuration for the Microsoft365 provider. fixer_config(self): Returns the fixer configuration. output_options(self, options: tuple): Sets the output options for the Microsoft365 provider. mutelist(self) -> Microsoft365Mutelist: Returns the mutelist object associated with the Microsoft365 provider. - validate_arguments(cls, az_cli_auth, app_env_auth, browser_auth, managed_identity_auth, tenant_id): Validates the authentication arguments for the Microsoft365 provider. setup_region_config(cls, region): Sets up the region configuration for the Microsoft365 provider. print_credentials(self): Prints the Microsoft365 credentials information. setup_session(cls, az_cli_auth, app_env_auth, browser_auth, managed_identity_auth, tenant_id, region_config): Set up the Microsoft365 session with the specified authentication method. @@ -85,14 +87,12 @@ class Microsoft365Provider(Provider): _identity: Microsoft365IdentityInfo _audit_config: dict _region_config: Microsoft365RegionConfig - _locations: dict _mutelist: Microsoft365Mutelist # TODO: this is not optional, enforce for all providers audit_metadata: Audit_Metadata def __init__( self, - app_env_auth: bool = False, tenant_id: str = None, region: str = "AzureCloud", client_id: str = None, @@ -132,23 +132,14 @@ def __init__( """ logger.info("Setting Microsoft365 provider ...") - logger.info("Checking if any credentials mode is set ...") - logger.info("Checking if region is different than default one") self._region_config = self.setup_region_config(region) # Set up the Microsoft365 session - self._session = self.setup_session( - app_env_auth, - ) + self._session = self.setup_session() # Set up the identity - self._identity = self.setup_identity( - app_env_auth, - ) - - # TODO: should we keep this here or within the identity? - self._locations = self.get_locations(self.session) + self._identity = self.setup_identity() # Audit Config if config_content: @@ -195,11 +186,6 @@ def region_config(self): """Returns the region configuration for the Microsoft365 provider.""" return self._region_config - @property - def locations(self): - """Returns a list of available locations for the Microsoft365 provider.""" - return self._locations - @property def audit_config(self): """Returns the audit configuration for the Microsoft365 provider.""" @@ -266,10 +252,6 @@ def print_credentials(self): Returns: None """ - printed_subscriptions = [] - for key, value in self._identity.subscriptions.items(): - intermediate = key + ": " + value - printed_subscriptions.append(intermediate) report_lines = [ f"Microsoft365 Region: {Fore.YELLOW}{self.region_config.name}{Style.RESET_ALL}", f"Microsoft365 Identity Type: {Fore.YELLOW}{self._identity.identity_type}{Style.RESET_ALL} Microsoft365 Identity ID: {Fore.YELLOW}{self._identity.identity_id}{Style.RESET_ALL}", @@ -282,9 +264,7 @@ def print_credentials(self): # TODO: setup_session or setup_credentials? # This should be setup_credentials, since it is setting up the credentials for the provider @staticmethod - def setup_session( - app_env_auth: bool, - ): + def setup_session(): """Returns the Microsoft365 credentials object. Set up the Microsoft365 session with the specified authentication method. @@ -299,22 +279,18 @@ def setup_session( Exception: If failed to retrieve Microsoft365 credentials. """ - # Browser auth creds cannot be set with DefaultMicrosoft365Credentials() - if app_env_auth: - try: - Microsoft365Provider.check_application_creds_env_vars() - credentials = ClientSecretCredential( - client_id=getenv("APP_CLIENT_ID"), - tenant_id=getenv("APP_TENANT_ID"), - client_secret=getenv("APP_CLIENT_SECRET"), - ) - except ( - Microsoft365EnvironmentVariableError - ) as environment_credentials_error: - logger.critical( - f"{environment_credentials_error.__class__.__name__}[{environment_credentials_error.__traceback__.tb_lineno}] -- {environment_credentials_error}" - ) - raise environment_credentials_error + try: + Microsoft365Provider.check_application_creds_env_vars() + credentials = ClientSecretCredential( + client_id=getenv("APP_CLIENT_ID"), + tenant_id=getenv("APP_TENANT_ID"), + client_secret=getenv("APP_CLIENT_SECRET"), + ) + except Microsoft365EnvironmentVariableError as environment_credentials_error: + logger.critical( + f"{environment_credentials_error.__class__.__name__}[{environment_credentials_error.__traceback__.tb_lineno}] -- {environment_credentials_error}" + ) + raise environment_credentials_error if not credentials: raise Microsoft365CredentialsUnavailableError( file=os.path.basename(__file__), @@ -322,6 +298,207 @@ def setup_session( ) return credentials + @staticmethod + def test_connection( + tenant_id=None, + region="AzureCloud", + raise_on_exception=True, + client_id=None, + client_secret=None, + provider_id=None, + ) -> Connection: + """Test connection to Azure subscription. + + Test the connection to an Azure subscription using the provided credentials. + + Args: + + tenant_id (str): The Azure Active Directory tenant ID. + region (str): The Azure region. + raise_on_exception (bool): Flag indicating whether to raise an exception if the connection fails. + client_id (str): The Azure client ID. + client_secret (str): The Azure client secret. + provider_id (str): The provider ID, in this case it's the Azure subscription ID. + + Returns: + bool: True if the connection is successful, False otherwise. + + Raises: + Exception: If failed to test the connection to Azure subscription. + AzureArgumentTypeValidationError: If there is an error in the argument type validation. + AzureSetUpRegionConfigError: If there is an error in setting up the region configuration. + AzureDefaultAzureCredentialError: If there is an error in retrieving the Azure credentials. + AzureInteractiveBrowserCredentialError: If there is an error in retrieving the Azure credentials using browser authentication. + AzureHTTPResponseError: If there is an HTTP response error. + AzureConfigCredentialsError: If there is an error in configuring the Azure credentials from a dictionary. + + + Examples: + >>> AzureProvider.test_connection(az_cli_auth=True) + True + >>> AzureProvider.test_connection(sp_env_auth=False, browser_auth=True, tenant_id=None) + False, ArgumentTypeError: Azure Tenant ID is required only for browser authentication mode + >>> AzureProvider.test_connection(tenant_id="XXXXXXXXXX", client_id="XXXXXXXXXX", client_secret="XXXXXXXXXX") + True + """ + try: + Microsoft365Provider.validate_arguments( + tenant_id, + client_id, + client_secret, + ) + region_config = Microsoft365Provider.setup_region_config(region) + + # Get the dict from the static credentials + Microsoft365_credentials = None + if tenant_id and client_id and client_secret: + Microsoft365_credentials = ( + Microsoft365Provider.validate_static_credentials( + tenant_id=tenant_id, + client_id=client_id, + client_secret=client_secret, + ) + ) + + # Set up the Microsoft365 session + Microsoft365Provider.setup_session( + tenant_id, + Microsoft365_credentials, + region_config, + ) + + logger.info( + "Microsoft365 provider: Connection to Microsoft365 subscription successful" + ) + + return Connection(is_connected=True) + + # Exceptions from setup_region_config + except Microsoft365ArgumentTypeValidationError as type_validation_error: + logger.error( + f"{type_validation_error.__class__.__name__}[{type_validation_error.__traceback__.tb_lineno}]: {type_validation_error}" + ) + if raise_on_exception: + raise type_validation_error + return Connection(error=type_validation_error) + except Microsoft365SetUpRegionConfigError as region_config_error: + logger.error( + f"{region_config_error.__class__.__name__}[{region_config_error.__traceback__.tb_lineno}]: {region_config_error}" + ) + if raise_on_exception: + raise region_config_error + return Connection(error=region_config_error) + # Exceptions from setup_session + except Microsoft365EnvironmentVariableError as environment_credentials_error: + logger.error( + f"{environment_credentials_error.__class__.__name__}[{environment_credentials_error.__traceback__.tb_lineno}]: {environment_credentials_error}" + ) + if raise_on_exception: + raise environment_credentials_error + return Connection(error=environment_credentials_error) + except ( + Microsoft365DefaultMicrosoft365CredentialError + ) as default_credentials_error: + logger.error( + f"{default_credentials_error.__class__.__name__}[{default_credentials_error.__traceback__.tb_lineno}]: {default_credentials_error}" + ) + if raise_on_exception: + raise default_credentials_error + return Connection(error=default_credentials_error) + except ( + Microsoft365InteractiveBrowserCredentialError + ) as interactive_browser_error: + logger.error( + f"{interactive_browser_error.__class__.__name__}[{interactive_browser_error.__traceback__.tb_lineno}]: {interactive_browser_error}" + ) + if raise_on_exception: + raise interactive_browser_error + return Connection(error=interactive_browser_error) + except Microsoft365ConfigCredentialsError as config_credentials_error: + logger.error( + f"{config_credentials_error.__class__.__name__}[{config_credentials_error.__traceback__.tb_lineno}]: {config_credentials_error}" + ) + if raise_on_exception: + raise config_credentials_error + return Connection(error=config_credentials_error) + except Microsoft365ClientAuthenticationError as client_auth_error: + logger.error( + f"{client_auth_error.__class__.__name__}[{client_auth_error.__traceback__.tb_lineno}]: {client_auth_error}" + ) + if raise_on_exception: + raise client_auth_error + return Connection(error=client_auth_error) + except Microsoft365CredentialsUnavailableError as credential_unavailable_error: + logger.error( + f"{credential_unavailable_error.__class__.__name__}[{credential_unavailable_error.__traceback__.tb_lineno}]: {credential_unavailable_error}" + ) + if raise_on_exception: + raise credential_unavailable_error + return Connection(error=credential_unavailable_error) + except ( + Microsoft365DefaultMicrosoft365CredentialError + ) as default_credentials_error: + logger.error( + f"{default_credentials_error.__class__.__name__}[{default_credentials_error.__traceback__.tb_lineno}]: {default_credentials_error}" + ) + if raise_on_exception: + raise default_credentials_error + return Connection(error=default_credentials_error) + except ( + Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError + ) as tenant_id_error: + logger.error( + f"{tenant_id_error.__class__.__name__}[{tenant_id_error.__traceback__.tb_lineno}]: {tenant_id_error}" + ) + if raise_on_exception: + raise tenant_id_error + return Connection(error=tenant_id_error) + except ( + Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError + ) as client_id_error: + logger.error( + f"{client_id_error.__class__.__name__}[{client_id_error.__traceback__.tb_lineno}]: {client_id_error}" + ) + if raise_on_exception: + raise client_id_error + return Connection(error=client_id_error) + except ( + Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError + ) as client_secret_error: + logger.error( + f"{client_secret_error.__class__.__name__}[{client_secret_error.__traceback__.tb_lineno}]: {client_secret_error}" + ) + if raise_on_exception: + raise client_secret_error + return Connection(error=client_secret_error) + # Exceptions from provider_id validation + except Microsoft365InvalidProviderIdError as invalid_credentials_error: + logger.error( + f"{invalid_credentials_error.__class__.__name__}[{invalid_credentials_error.__traceback__.tb_lineno}]: {invalid_credentials_error}" + ) + if raise_on_exception: + raise invalid_credentials_error + return Connection(error=invalid_credentials_error) + # Exceptions from SubscriptionClient + except HttpResponseError as http_response_error: + logger.error( + f"{http_response_error.__class__.__name__}[{http_response_error.__traceback__.tb_lineno}]: {http_response_error}" + ) + if raise_on_exception: + raise Microsoft365HTTPResponseError( + file=os.path.basename(__file__), + original_exception=http_response_error, + ) + return Connection(error=http_response_error) + except Exception as error: + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + if raise_on_exception: + # Raise directly the exception + raise error + return Connection(error=error) + @staticmethod def check_application_creds_env_vars(): """ @@ -349,7 +526,6 @@ def check_application_creds_env_vars(): def setup_identity( self, - app_env_auth, ): """ Sets up the identity for the Microsoft365 provider. @@ -368,99 +544,48 @@ def setup_identity( # the identity can access AAD and retrieve the tenant domain name. # With cli also should be possible but right now it does not work, microsoft365 python package issue is coming # At the time of writting this with az cli creds is not working, despite that is included - if app_env_auth: - async def get_microsoft365_identity(): - # Trying to recover tenant domain info - try: - logger.info( - "Trying to retrieve tenant domain from AAD to populate identity structure ..." - ) - client = GraphServiceClient(credentials=credentials) - - domain_result = await client.domains.get() - if getattr(domain_result, "value"): - if getattr(domain_result.value[0], "id"): - identity.tenant_domain = domain_result.value[0].id - - except HttpResponseError as error: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - raise Microsoft365HTTPResponseError( - file=os.path.basename(__file__), - original_exception=error, - ) - except ClientAuthenticationError as error: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - raise Microsoft365GetTokenIdentityError( - file=os.path.basename(__file__), - original_exception=error, - ) - except Exception as error: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - # since that exception is not considered as critical, we keep filling another identity fields - if app_env_auth: - # The id of the sp can be retrieved from environment variables - identity.identity_id = getenv("APP_CLIENT_ID") - identity.identity_type = "Application" - # Same here, if user can access AAD, some fields are retrieved if not, default value, for az cli - # should work but it doesn't, pending issue - else: - identity.identity_id = "Unknown user id (Missing AAD permissions)" - identity.identity_type = "User" - try: - logger.info( - "Trying to retrieve user information from AAD to populate identity structure ..." - ) - client = GraphServiceClient(credentials=credentials) - - me = await client.me.get() - if me: - if getattr(me, "user_principal_name"): - identity.identity_id = me.user_principal_name - - except Exception as error: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - - asyncio.get_event_loop().run_until_complete(get_microsoft365_identity()) + async def get_microsoft365_identity(): + # Trying to recover tenant domain info + try: + logger.info( + "Trying to retrieve tenant domain from AAD to populate identity structure ..." + ) + client = GraphServiceClient(credentials=credentials) - return identity + domain_result = await client.domains.get() + if getattr(domain_result, "value"): + if getattr(domain_result.value[0], "id"): + identity.tenant_domain = domain_result.value[0].id - def get_locations(self, credentials) -> dict[str, list[str]]: - """ - Retrieves the locations available for each subscription using the provided credentials. + except HttpResponseError as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365HTTPResponseError( + file=os.path.basename(__file__), + original_exception=error, + ) + except ClientAuthenticationError as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365GetTokenIdentityError( + file=os.path.basename(__file__), + original_exception=error, + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + # since that exception is not considered as critical, we keep filling another identity fields + # The id of the sp can be retrieved from environment variables + identity.identity_id = getenv("APP_CLIENT_ID") + identity.identity_type = "Application" - Args: - credentials: The credentials object used to authenticate the request. + asyncio.get_event_loop().run_until_complete(get_microsoft365_identity()) - Returns: - A dictionary containing the locations available for each subscription. The dictionary - has subscription display names as keys and lists of location names as values. - """ - locations = None - if credentials: - locations = {} - token = credentials.get_token("https://management.azure.com/.default").token - for display_name, subscription_id in self._identity.subscriptions.items(): - locations.update({display_name: []}) - url = f"https://management.azure.com/subscriptions/{subscription_id}/locations?api-version=2022-12-01" - headers = { - "Authorization": f"Bearer {token}", - "Content-Type": "application/json", - } - response = requests.get(url, headers=headers) - if response.status_code == 200: - data = response.json() - for location in data["value"]: - locations[display_name].append(location["name"]) - return locations + return identity @staticmethod def validate_static_credentials( diff --git a/prowler/providers/microsoft365/models.py b/prowler/providers/microsoft365/models.py index e17a836cca5..9e948eaae5c 100644 --- a/prowler/providers/microsoft365/models.py +++ b/prowler/providers/microsoft365/models.py @@ -7,10 +7,8 @@ class Microsoft365IdentityInfo(BaseModel): identity_id: str = "" identity_type: str = "" - tenant_ids: list[str] = [] + tenant_id: str = "" tenant_domain: str = "Unknown tenant domain (missing AAD permissions)" - subscriptions: dict = {} - locations: dict = {} class Microsoft365RegionConfig(BaseModel): @@ -38,6 +36,8 @@ def __init__(self, arguments, bulk_checks_metadata, identity): f"prowler-output-{identity.tenant_domain}-{output_file_timestamp}" ) else: - self.output_filename = f"prowler-output-{'-'.join(identity.tenant_ids)}-{output_file_timestamp}" + self.output_filename = ( + f"prowler-output-{identity.tenant_id}-{output_file_timestamp}" + ) else: self.output_filename = arguments.output_filename From 88608953a98e5ce1c14105c9bc6fb34038d12245 Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Thu, 28 Nov 2024 20:36:04 +0100 Subject: [PATCH 09/44] feat(admincenter): Delete one attribute from the user that was not going to be used and add more description to status extended in admincenter checks --- .../microsoft365/services/admincenter/admincenter_service.py | 5 ++--- .../admincenter_users_admins_reduced_license_footprint.py | 2 +- .../admincenter_users_between_two_and_four_global_admins.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_service.py b/prowler/providers/microsoft365/services/admincenter/admincenter_service.py index d63dc2c70f0..2b93b22d5b8 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_service.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_service.py @@ -43,10 +43,10 @@ async def _get_users(self): mailbox_settings = await client.users.by_user_id( user.id ).mailbox_settings.get() - user_type = mailbox_settings.user_purpose + mailbox_settings.user_purpose except ODataError as error: if error.error.code == "MailboxNotEnabledForRESTAPI": - user_type = "inactive" + pass else: logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -61,7 +61,6 @@ async def _get_users(self): if license_details.value else None ), - user_type=user_type, ) } ) diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py index 53756891024..7685abeca85 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py @@ -22,7 +22,7 @@ def execute(self) -> Check_Report_Microsoft365: report.resource_name = user.name report.tenant_id = tenant_domain report.status = "FAIL" - report.status_extended = f"User {user.name} has administrative roles {admin_roles} and license {user.license}." + report.status_extended = f"User {user.name} has administrative roles {admin_roles} and an invalid license {user.license}." if user.license in allowed_licenses: report.status = "PASS" diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py index e6186605557..97e23b50610 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py @@ -34,7 +34,7 @@ def execute(self) -> Check_Report_Microsoft365: f"There are {num_global_admins} global administrators." ) else: - report.status_extended = f"There are {num_global_admins} global administrators. It should be more than two and less than five." + report.status_extended = f"There are {num_global_admins} global administrators. It should be more than one and less than five." findings.append(report) From 13f6139413cba6eee4627a4bc9a5f05f8fa53d11 Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Thu, 28 Nov 2024 20:38:55 +0100 Subject: [PATCH 10/44] feat(tests): Add service and checks tests working properly, provider tests still not working --- .../fixtures/microsoft365_mutelist.yaml | 16 + .../mutelist/microsoft365_mutelist_test.py | 100 +++++++ .../microsoft365/lib/regions/regions_test.py | 38 +++ .../microsoft365/microsoft365_fixtures.py | 37 +++ .../microsoft365_provider_test.py | 275 ++++++++++++++++++ ...enter_groups_not_public_visibility_test.py | 115 ++++++++ .../admincenter/admincenter_service_test.py | 98 +++++++ ...s_admins_reduced_license_footprint_test.py | 165 +++++++++++ ...between_two_and_four_global_admins_test.py | 190 ++++++++++++ .../admincenter/lib/user_privileges_test.py | 25 ++ 10 files changed, 1059 insertions(+) create mode 100644 tests/providers/microsoft365/lib/mutelist/fixtures/microsoft365_mutelist.yaml create mode 100644 tests/providers/microsoft365/lib/mutelist/microsoft365_mutelist_test.py create mode 100644 tests/providers/microsoft365/lib/regions/regions_test.py create mode 100644 tests/providers/microsoft365/microsoft365_fixtures.py create mode 100644 tests/providers/microsoft365/microsoft365_provider_test.py create mode 100644 tests/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility_test.py create mode 100644 tests/providers/microsoft365/services/admincenter/admincenter_service_test.py create mode 100644 tests/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint_test.py create mode 100644 tests/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins_test.py create mode 100644 tests/providers/microsoft365/services/admincenter/lib/user_privileges_test.py diff --git a/tests/providers/microsoft365/lib/mutelist/fixtures/microsoft365_mutelist.yaml b/tests/providers/microsoft365/lib/mutelist/fixtures/microsoft365_mutelist.yaml new file mode 100644 index 00000000000..69fb405006f --- /dev/null +++ b/tests/providers/microsoft365/lib/mutelist/fixtures/microsoft365_mutelist.yaml @@ -0,0 +1,16 @@ +### Account, Check and/or Region can be * to apply for all the cases. +### Resources and tags are lists that can have either Regex or Keywords. +### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together. +### Use an alternation Regex to match one of multiple tags with "ORed" logic. +### For each check you can except Accounts, Regions, Resources and/or Tags. +########################### MUTELIST EXAMPLE ########################### +Mutelist: + Accounts: + "subscription_1": + Checks: + "aks_cluster_rbac_enabled": + Regions: + - "*" + Resources: + - "resource_1" + - "resource_2" diff --git a/tests/providers/microsoft365/lib/mutelist/microsoft365_mutelist_test.py b/tests/providers/microsoft365/lib/mutelist/microsoft365_mutelist_test.py new file mode 100644 index 00000000000..b2572979555 --- /dev/null +++ b/tests/providers/microsoft365/lib/mutelist/microsoft365_mutelist_test.py @@ -0,0 +1,100 @@ +import yaml +from mock import MagicMock + +from prowler.providers.microsoft365.lib.mutelist.mutelist import Microsoft365Mutelist +from tests.lib.outputs.fixtures.fixtures import generate_finding_output + +MUTELIST_FIXTURE_PATH = ( + "tests/providers/microsoft365/lib/mutelist/fixtures/azure_mutelist.yaml" +) + + +class TestMicrosoft365Mutelist: + def test_get_mutelist_file_from_local_file(self): + mutelist = Microsoft365Mutelist(mutelist_path=MUTELIST_FIXTURE_PATH) + + with open(MUTELIST_FIXTURE_PATH) as f: + mutelist_fixture = yaml.safe_load(f)["Mutelist"] + + assert mutelist.mutelist == mutelist_fixture + assert mutelist.mutelist_file_path == MUTELIST_FIXTURE_PATH + + def test_get_mutelist_file_from_local_file_non_existent(self): + mutelist_path = "tests/lib/mutelist/fixtures/not_present" + mutelist = Microsoft365Mutelist(mutelist_path=mutelist_path) + + assert mutelist.mutelist == {} + assert mutelist.mutelist_file_path == mutelist_path + + def test_validate_mutelist_not_valid_key(self): + mutelist_path = MUTELIST_FIXTURE_PATH + with open(mutelist_path) as f: + mutelist_fixture = yaml.safe_load(f)["Mutelist"] + + mutelist_fixture["Accounts1"] = mutelist_fixture["Accounts"] + del mutelist_fixture["Accounts"] + + mutelist = Microsoft365Mutelist(mutelist_content=mutelist_fixture) + + assert not mutelist.validate_mutelist() + assert mutelist.mutelist == {} + assert mutelist.mutelist_file_path is None + + def test_is_finding_muted(self): + # Mutelist + mutelist_content = { + "Accounts": { + "subscription_1": { + "Checks": { + "check_test": { + "Regions": ["*"], + "Resources": ["test_resource"], + } + } + } + } + } + + mutelist = Microsoft365Mutelist(mutelist_content=mutelist_content) + + finding = MagicMock + finding.check_metadata = MagicMock + finding.check_metadata.CheckID = "check_test" + finding.status = "FAIL" + finding.resource_name = "test_resource" + finding.resource_tags = [] + + assert mutelist.is_finding_muted(finding) + + def test_mute_finding(self): + # Mutelist + mutelist_content = { + "Accounts": { + "subscription_1": { + "Checks": { + "check_test": { + "Regions": ["*"], + "Resources": ["test_resource"], + } + } + } + } + } + + mutelist = Microsoft365Mutelist(mutelist_content=mutelist_content) + + finding_1 = generate_finding_output( + check_id="check_test", + status="FAIL", + account_uid="subscription_1", + region="subscription_1", + resource_uid="test_resource", + resource_tags=[], + muted=False, + ) + + muted_finding = mutelist.mute_finding(finding=finding_1) + + assert muted_finding.status == "MUTED" + assert muted_finding.muted is True + assert muted_finding.raw["status"] == "FAIL" diff --git a/tests/providers/microsoft365/lib/regions/regions_test.py b/tests/providers/microsoft365/lib/regions/regions_test.py new file mode 100644 index 00000000000..2f8fdd60533 --- /dev/null +++ b/tests/providers/microsoft365/lib/regions/regions_test.py @@ -0,0 +1,38 @@ +from azure.identity import AzureAuthorityHosts + +from prowler.providers.azure.lib.regions.regions import ( + AZURE_CHINA_CLOUD, + AZURE_GENERIC_CLOUD, + AZURE_US_GOV_CLOUD, + get_regions_config, +) + + +class Test_azure_regions: + def test_get_regions_config(self): + allowed_regions = [ + "AzureCloud", + "AzureChinaCloud", + "AzureUSGovernment", + ] + expected_output = { + "AzureCloud": { + "authority": None, + "base_url": AZURE_GENERIC_CLOUD, + "credential_scopes": [AZURE_GENERIC_CLOUD + "/.default"], + }, + "AzureChinaCloud": { + "authority": AzureAuthorityHosts.AZURE_CHINA, + "base_url": AZURE_CHINA_CLOUD, + "credential_scopes": [AZURE_CHINA_CLOUD + "/.default"], + }, + "AzureUSGovernment": { + "authority": AzureAuthorityHosts.AZURE_GOVERNMENT, + "base_url": AZURE_US_GOV_CLOUD, + "credential_scopes": [AZURE_US_GOV_CLOUD + "/.default"], + }, + } + + for region in allowed_regions: + region_config = get_regions_config(region) + assert region_config == expected_output[region] diff --git a/tests/providers/microsoft365/microsoft365_fixtures.py b/tests/providers/microsoft365/microsoft365_fixtures.py new file mode 100644 index 00000000000..5d6b30caf9d --- /dev/null +++ b/tests/providers/microsoft365/microsoft365_fixtures.py @@ -0,0 +1,37 @@ +from azure.identity import DefaultAzureCredential +from mock import MagicMock + +from prowler.providers.microsoft365.microsoft365_provider import Microsoft365Provider +from prowler.providers.microsoft365.models import ( + Microsoft365IdentityInfo, + Microsoft365RegionConfig, +) + +# Azure Identity +IDENTITY_ID = "00000000-0000-0000-0000-000000000000" +IDENTITY_TYPE = "Application" +TENANT_ID = "00000000-0000-0000-0000-000000000000" +DOMAIN = "user.onmicrosoft.com" + + +# Mocked Azure Audit Info +def set_mocked_microsoft365_provider( + credentials: DefaultAzureCredential = DefaultAzureCredential(), + identity: Microsoft365IdentityInfo = Microsoft365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type=IDENTITY_TYPE, + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + ), + audit_config: dict = None, + azure_region_config: Microsoft365RegionConfig = Microsoft365RegionConfig(), +) -> Microsoft365Provider: + + provider = MagicMock() + provider.type = "microsoft365" + provider.session.credentials = credentials + provider.identity = identity + provider.audit_config = audit_config + provider.region_config = azure_region_config + + return provider diff --git a/tests/providers/microsoft365/microsoft365_provider_test.py b/tests/providers/microsoft365/microsoft365_provider_test.py new file mode 100644 index 00000000000..6ed01f1b4c3 --- /dev/null +++ b/tests/providers/microsoft365/microsoft365_provider_test.py @@ -0,0 +1,275 @@ +from unittest.mock import patch +from uuid import uuid4 + +import pytest +from azure.core.credentials import AccessToken +from azure.identity import DefaultAzureCredential +from mock import MagicMock + +from prowler.config.config import ( + default_config_file_path, + default_fixer_config_file_path, + load_and_validate_config_file, +) +from prowler.providers.common.models import Connection +from prowler.providers.microsoft365.exceptions.exceptions import ( + Microsoft365HTTPResponseError, + Microsoft365InvalidProviderIdError, + Microsoft365NoAuthenticationMethodError, +) +from prowler.providers.microsoft365.microsoft365_provider import Microsoft365Provider +from prowler.providers.microsoft365.models import ( + Microsoft365IdentityInfo, + Microsoft365RegionConfig, +) + + +class TestMicrosoft365Provider: + def test_microsoft365_provider(self): + tenant_id = None + client_id = None + client_secret = None + + fixer_config = load_and_validate_config_file( + "microsoft365", default_fixer_config_file_path + ) + azure_region = "AzureCloud" + + with patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_identity", + return_value=Microsoft365IdentityInfo(), + ): + microsoft365_provider = Microsoft365Provider( + tenant_id, + azure_region, + config_path=default_config_file_path, + fixer_config=fixer_config, + client_id=client_id, + client_secret=client_secret, + ) + + assert microsoft365_provider.region_config == Microsoft365RegionConfig( + name="AzureCloud", + authority=None, + base_url="https://management.azure.com", + credential_scopes=["https://management.azure.com/.default"], + ) + assert isinstance(microsoft365_provider.session, DefaultAzureCredential) + assert microsoft365_provider.identity == Microsoft365IdentityInfo( + identity_id="", + identity_type="", + tenant_id="", + tenant_domain="Unknown tenant domain (missing AAD permissions)", + ) + assert microsoft365_provider.audit_config == { + "shodan_api_key": None, + "php_latest_version": "8.2", + "python_latest_version": "3.12", + "java_latest_version": "17", + } + + def test_test_connection_tenant_id_client_id_client_secret(self): + with patch( + "prowler.providers.microsoft365.microsoft365_provider.DefaultAzureCredential" + ) as mock_default_credential, patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" + ) as mock_setup_session, patch( + "prowler.providers.microsoft365.microsoft365_provider.SubscriptionClient" + ) as mock_resource_client, patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.validate_static_credentials" + ) as mock_validate_static_credentials: + + # Mock the return value of DefaultAzureCredential + mock_credentials = MagicMock() + mock_credentials.get_token.return_value = AccessToken( + token="fake_token", expires_on=9999999999 + ) + mock_default_credential.return_value = { + "client_id": str(uuid4()), + "client_secret": str(uuid4()), + "tenant_id": str(uuid4()), + } + + # Mock setup_session to return a mocked session object + mock_session = MagicMock() + mock_setup_session.return_value = mock_session + + # Mock ValidateStaticCredentials to avoid real API calls + mock_validate_static_credentials.return_value = None + + # Mock ResourceManagementClient to avoid real API calls + mock_client = MagicMock() + mock_resource_client.return_value = mock_client + + test_connection = Microsoft365Provider.test_connection( + browser_auth=False, + tenant_id=str(uuid4()), + region="AzureCloud", + raise_on_exception=False, + client_id=str(uuid4()), + client_secret=str(uuid4()), + ) + + assert isinstance(test_connection, Connection) + assert test_connection.is_connected + assert test_connection.error is None + + def test_test_connection_provider_validation(self): + with patch( + "prowler.providers.microsoft365.microsoft365_provider.DefaultAzureCredential" + ) as mock_default_credential, patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" + ) as mock_setup_session, patch( + "prowler.providers.microsoft365.microsoft365_provider.SubscriptionClient" + ) as mock_resource_client, patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.validate_static_credentials" + ) as mock_validate_static_credentials: + + # Mock the return value of DefaultAzureCredential + mock_default_credential.return_value = { + "client_id": str(uuid4()), + "client_secret": str(uuid4()), + "tenant_id": str(uuid4()), + } + + # Mock setup_session to return a mocked session object + mock_session = MagicMock() + mock_setup_session.return_value = mock_session + + # Mock ValidateStaticCredentials to avoid real API calls + mock_validate_static_credentials.return_value = None + + # Mock ResourceManagementClient to avoid real API calls + mock_subscription = MagicMock() + mock_subscription.subscription_id = "test_provider_id" + mock_return_value = MagicMock() + mock_return_value.subscriptions.list.return_value = [mock_subscription] + mock_resource_client.return_value = mock_return_value + + test_connection = Microsoft365Provider.test_connection( + browser_auth=False, + tenant_id=str(uuid4()), + region="AzureCloud", + raise_on_exception=False, + client_id=str(uuid4()), + client_secret=str(uuid4()), + provider_id="test_provider_id", + ) + + assert isinstance(test_connection, Connection) + assert test_connection.is_connected + assert test_connection.error is None + + def test_test_connection_provider_validation_error(self): + with patch( + "prowler.providers.microsoft365.microsoft365_provider.DefaultAzureCredential" + ) as mock_default_credential, patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" + ) as mock_setup_session, patch( + "prowler.providers.microsoft365.microsoft365_provider.SubscriptionClient" + ) as mock_resource_client, patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.validate_static_credentials" + ) as mock_validate_static_credentials: + + # Mock the return value of DefaultAzureCredential + mock_default_credential.return_value = { + "client_id": str(uuid4()), + "client_secret": str(uuid4()), + "tenant_id": str(uuid4()), + } + + # Mock setup_session to return a mocked session object + mock_session = MagicMock() + mock_setup_session.return_value = mock_session + + # Mock ValidateStaticCredentials to avoid real API calls + mock_validate_static_credentials.return_value = None + + # Mock ResourceManagementClient to avoid real API calls + mock_subscription = MagicMock() + mock_subscription.subscription_id = "test_invalid_provider_id" + mock_return_value = MagicMock() + mock_return_value.subscriptions.list.return_value = [mock_subscription] + mock_resource_client.return_value = mock_return_value + + test_connection = Microsoft365Provider.test_connection( + browser_auth=False, + tenant_id=str(uuid4()), + region="AzureCloud", + raise_on_exception=False, + client_id=str(uuid4()), + client_secret=str(uuid4()), + provider_id="test_provider_id", + ) + + assert test_connection.error is not None + assert isinstance(test_connection.error, Microsoft365InvalidProviderIdError) + assert ( + "The provided credentials are not valid for the specified Microsoft365 subscription." + in test_connection.error.args[0] + ) + + def test_test_connection_with_ClientAuthenticationError(self): + with pytest.raises(Microsoft365HTTPResponseError) as exception: + tenant_id = str(uuid4()) + Microsoft365Provider.test_connection( + browser_auth=True, + tenant_id=tenant_id, + region="AzureCloud", + ) + + assert exception.type == Microsoft365HTTPResponseError + assert ( + exception.value.args[0] + == f"[2010] Error in HTTP response from Microsoft365 - Authentication failed: Unable to get authority configuration for https://login.microsoftonline.com/{tenant_id}. Authority would typically be in a format of https://login.microsoftonline.com/your_tenant or https://tenant_name.ciamlogin.com or https://tenant_name.b2clogin.com/tenant.onmicrosoft.com/policy. Also please double check your tenant name or GUID is correct." + ) + + def test_test_connection_without_any_method(self): + with pytest.raises(Microsoft365NoAuthenticationMethodError) as exception: + Microsoft365Provider.test_connection() + + assert exception.type == Microsoft365NoAuthenticationMethodError + assert ( + "[2003] Microsoft365 provider requires at least one authentication method set: [--az-cli-auth | --sp-env-auth | --browser-auth | --managed-identity-auth]" + in exception.value.args[0] + ) + + def test_test_connection_with_httpresponseerror(self): + with patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.get_locations", + return_value={}, + ), patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" + ) as mock_setup_session: + + mock_setup_session.side_effect = Microsoft365HTTPResponseError( + file="test_file", original_exception="Simulated HttpResponseError" + ) + + with pytest.raises(Microsoft365HTTPResponseError) as exception: + Microsoft365Provider.test_connection( + az_cli_auth=True, + raise_on_exception=True, + ) + + assert exception.type == Microsoft365HTTPResponseError + assert ( + exception.value.args[0] + == "[2010] Error in HTTP response from Microsoft365 - Simulated HttpResponseError" + ) + + def test_test_connection_with_exception(self): + with patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" + ) as mock_setup_session: + + mock_setup_session.side_effect = Exception("Simulated Exception") + + with pytest.raises(Exception) as exception: + Microsoft365Provider.test_connection( + sp_env_auth=True, + raise_on_exception=True, + ) + + assert exception.type == Exception + assert exception.value.args[0] == "Simulated Exception" diff --git a/tests/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility_test.py b/tests/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility_test.py new file mode 100644 index 00000000000..074b3142a22 --- /dev/null +++ b/tests/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility_test.py @@ -0,0 +1,115 @@ +from unittest import mock +from uuid import uuid4 + +from tests.providers.microsoft365.microsoft365_fixtures import ( + DOMAIN, + set_mocked_microsoft365_provider, +) + + +class Test_admincenter_groups_not_public_visibility: + def test_admincenter_no_tenants(self): + admincenter_client = mock.MagicMock + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility import ( + admincenter_groups_not_public_visibility, + ) + + admincenter_client.groups = {} + + check = admincenter_groups_not_public_visibility() + result = check.execute() + assert len(result) == 0 + + def test_admincenter_tenant_empty(self): + admincenter_client = mock.MagicMock + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility import ( + admincenter_groups_not_public_visibility, + ) + + admincenter_client.groups = {DOMAIN: {}} + + check = admincenter_groups_not_public_visibility() + result = check.execute() + assert len(result) == 0 + + def test_admincenter_user_no_admin(self): + admincenter_client = mock.MagicMock + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility import ( + admincenter_groups_not_public_visibility, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + Group, + ) + + id_user1 = str(uuid4()) + + admincenter_client.groups = { + DOMAIN: { + id_user1: Group(id=id_user1, name="Group1", visibility="Private"), + } + } + + check = admincenter_groups_not_public_visibility() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == "Group Group1 has Private visibility." + assert result[0].resource_name == "Group1" + assert result[0].resource_id == id_user1 + + def test_admincenter_user_admin_compliant_license(self): + admincenter_client = mock.MagicMock + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility import ( + admincenter_groups_not_public_visibility, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + Group, + ) + + id_user1 = str(uuid4()) + + admincenter_client.groups = { + DOMAIN: { + id_user1: Group(id=id_user1, name="Group1", visibility="Private"), + } + } + + check = admincenter_groups_not_public_visibility() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == "Group Group1 has Private visibility." + assert result[0].resource_name == "Group1" + assert result[0].resource_id == id_user1 diff --git a/tests/providers/microsoft365/services/admincenter/admincenter_service_test.py b/tests/providers/microsoft365/services/admincenter/admincenter_service_test.py new file mode 100644 index 00000000000..ad16429efa4 --- /dev/null +++ b/tests/providers/microsoft365/services/admincenter/admincenter_service_test.py @@ -0,0 +1,98 @@ +from unittest.mock import patch + +from prowler.providers.azure.models import AzureIdentityInfo +from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + AdminCenter, + DirectoryRole, + Group, + User, +) +from tests.providers.microsoft365.microsoft365_fixtures import ( + DOMAIN, + set_mocked_microsoft365_provider, +) + + +async def mock_admincenter_get_users(_): + return { + DOMAIN: { + "user-1@tenant1.es": User( + id="id-1", + name="User 1", + directory_roles=[], + ), + } + } + + +async def mock_admincenter_get_directory_roles(_): + return { + DOMAIN: { + "GlobalAdministrator": DirectoryRole( + id="id-directory-role", + name="GlobalAdministrator", + members=[], + ) + } + } + + +async def mock_admincenter_get_groups(_): + return { + DOMAIN: { + "id-1": Group(id="id-1", name="Test", visibility="Public"), + } + } + + +@patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_service.AdminCenter._get_users", + new=mock_admincenter_get_users, +) +@patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_service.AdminCenter._get_directory_roles", + new=mock_admincenter_get_directory_roles, +) +@patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_service.AdminCenter._get_groups", + new=mock_admincenter_get_groups, +) +class Test_AdminCenter_Service: + def test_get_client(self): + admincenter_client = AdminCenter( + set_mocked_microsoft365_provider( + identity=AzureIdentityInfo(tenant_domain=DOMAIN) + ) + ) + assert ( + admincenter_client.clients[DOMAIN].__class__.__name__ + == "GraphServiceClient" + ) + + def test_get_users(self): + admincenter_client = AdminCenter(set_mocked_microsoft365_provider()) + assert len(admincenter_client.users) == 1 + assert admincenter_client.users[DOMAIN]["user-1@tenant1.es"].id == "id-1" + assert admincenter_client.users[DOMAIN]["user-1@tenant1.es"].name == "User 1" + + def test_get_group_settings(self): + admincenter_client = AdminCenter(set_mocked_microsoft365_provider()) + assert len(admincenter_client.groups) == 1 + assert admincenter_client.groups[DOMAIN]["id-1"].id == "id-1" + assert admincenter_client.groups[DOMAIN]["id-1"].name == "Test" + assert admincenter_client.groups[DOMAIN]["id-1"].visibility == "Public" + + def test_get_directory_roles(self): + admincenter_client = AdminCenter(set_mocked_microsoft365_provider()) + assert ( + admincenter_client.directory_roles[DOMAIN]["GlobalAdministrator"].id + == "id-directory-role" + ) + assert ( + len( + admincenter_client.directory_roles[DOMAIN][ + "GlobalAdministrator" + ].members + ) + == 0 + ) diff --git a/tests/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint_test.py b/tests/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint_test.py new file mode 100644 index 00000000000..2098de38f31 --- /dev/null +++ b/tests/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint_test.py @@ -0,0 +1,165 @@ +from unittest import mock +from uuid import uuid4 + +from tests.providers.microsoft365.microsoft365_fixtures import ( + DOMAIN, + set_mocked_microsoft365_provider, +) + + +class Test_admincenter_users_admins_reduced_license_footprint: + def test_admincenter_no_tenants(self): + admincenter_client = mock.MagicMock + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint import ( + admincenter_users_admins_reduced_license_footprint, + ) + + admincenter_client.users = {} + + check = admincenter_users_admins_reduced_license_footprint() + result = check.execute() + assert len(result) == 0 + + def test_admincenter_tenant_empty(self): + admincenter_client = mock.MagicMock + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint import ( + admincenter_users_admins_reduced_license_footprint, + ) + + admincenter_client.users = {DOMAIN: {}} + + check = admincenter_users_admins_reduced_license_footprint() + result = check.execute() + assert len(result) == 0 + + def test_admincenter_user_no_admin(self): + admincenter_client = mock.MagicMock + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + User, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint import ( + admincenter_users_admins_reduced_license_footprint, + ) + + id_user1 = str(uuid4()) + + admincenter_client.users = { + DOMAIN: { + id_user1: User( + id=id_user1, + name="User1", + directory_roles=["Exchange User"], + license="O365 BUSINESS", + ), + } + } + + check = admincenter_users_admins_reduced_license_footprint() + result = check.execute() + assert len(result) == 0 + + def test_admincenter_user_admin_compliant_license(self): + admincenter_client = mock.MagicMock + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + User, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint import ( + admincenter_users_admins_reduced_license_footprint, + ) + + id_user1 = str(uuid4()) + + admincenter_client.users = { + DOMAIN: { + id_user1: User( + id=id_user1, + name="User1", + directory_roles=["Global Administrator"], + license="AAD_PREMIUM", + ), + } + } + + check = admincenter_users_admins_reduced_license_footprint() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "User User1 has administrative roles ['Global Administrator'] and a valid license: AAD_PREMIUM." + ) + assert result[0].resource_name == "User1" + assert result[0].resource_id == id_user1 + + def test_admincenter_user_admin_non_compliant_license(self): + admincenter_client = mock.MagicMock + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + User, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint import ( + admincenter_users_admins_reduced_license_footprint, + ) + + id_user1 = str(uuid4()) + + admincenter_client.users = { + DOMAIN: { + id_user1: User( + id=id_user1, + name="User1", + directory_roles=["Global Administrator"], + license="O365 BUSINESS", + ), + } + } + + check = admincenter_users_admins_reduced_license_footprint() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "User User1 has administrative roles ['Global Administrator'] and an invalid license O365 BUSINESS." + ) + assert result[0].resource_name == "User1" + assert result[0].resource_id == id_user1 diff --git a/tests/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins_test.py b/tests/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins_test.py new file mode 100644 index 00000000000..da9bf37799f --- /dev/null +++ b/tests/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins_test.py @@ -0,0 +1,190 @@ +from unittest import mock +from uuid import uuid4 + +from tests.providers.microsoft365.microsoft365_fixtures import ( + DOMAIN, + set_mocked_microsoft365_provider, +) + + +class Test_admincenter_users_between_two_and_four_global_admins: + def test_admincenter_no_tenants(self): + admincenter_client = mock.MagicMock + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins import ( + admincenter_users_between_two_and_four_global_admins, + ) + + admincenter_client.directory_roles = {} + + check = admincenter_users_between_two_and_four_global_admins() + result = check.execute() + assert len(result) == 0 + + def test_admincenter_tenant_empty(self): + admincenter_client = mock.MagicMock + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins import ( + admincenter_users_between_two_and_four_global_admins, + ) + + admincenter_client.directory_roles = {DOMAIN: {}} + + check = admincenter_users_between_two_and_four_global_admins() + result = check.execute() + assert len(result) == 0 + + def test_admincenter_less_than_five_global_admins(self): + admincenter_client = mock.MagicMock + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + DirectoryRole, + User, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins import ( + admincenter_users_between_two_and_four_global_admins, + ) + + id = str(uuid4()) + id_user1 = str(uuid4()) + id_user2 = str(uuid4()) + + admincenter_client.directory_roles = { + DOMAIN: { + "Global Administrator": DirectoryRole( + id=id, + name="Global Administrator", + members=[ + User(id=id_user1, name="User1"), + User(id=id_user2, name="User2"), + ], + ) + } + } + + check = admincenter_users_between_two_and_four_global_admins() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == "There are 2 global administrators." + assert result[0].resource_name == "Global Administrator" + assert result[0].resource_id == id + + def test_admincenter_more_than_five_global_admins(self): + admincenter_client = mock.MagicMock + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + DirectoryRole, + User, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins import ( + admincenter_users_between_two_and_four_global_admins, + ) + + id = str(uuid4()) + id_user1 = str(uuid4()) + id_user2 = str(uuid4()) + id_user3 = str(uuid4()) + id_user4 = str(uuid4()) + id_user5 = str(uuid4()) + id_user6 = str(uuid4()) + + admincenter_client.directory_roles = { + DOMAIN: { + "Global Administrator": DirectoryRole( + id=id, + name="Global Administrator", + members=[ + User(id=id_user1, name="User1"), + User(id=id_user2, name="User2"), + User(id=id_user3, name="User3"), + User(id=id_user4, name="User4"), + User(id=id_user5, name="User5"), + User(id=id_user6, name="User6"), + ], + ) + } + } + + check = admincenter_users_between_two_and_four_global_admins() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "There are 6 global administrators. It should be more than one and less than five." + ) + assert result[0].resource_name == "Global Administrator" + assert result[0].resource_id == id + + def test_admincenter_one_global_admin(self): + admincenter_client = mock.MagicMock + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins.admincenter_client", + new=admincenter_client, + ): + from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( + DirectoryRole, + User, + ) + from prowler.providers.microsoft365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins import ( + admincenter_users_between_two_and_four_global_admins, + ) + + id = str(uuid4()) + id_user1 = str(uuid4()) + + admincenter_client.directory_roles = { + DOMAIN: { + "Global Administrator": DirectoryRole( + id=id, + name="Global Administrator", + members=[ + User(id=id_user1, name="User1"), + ], + ) + } + } + + check = admincenter_users_between_two_and_four_global_admins() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "There are 1 global administrators. It should be more than one and less than five." + ) + assert result[0].resource_name == "Global Administrator" + assert result[0].resource_id == id diff --git a/tests/providers/microsoft365/services/admincenter/lib/user_privileges_test.py b/tests/providers/microsoft365/services/admincenter/lib/user_privileges_test.py new file mode 100644 index 00000000000..bf2a75e44c4 --- /dev/null +++ b/tests/providers/microsoft365/services/admincenter/lib/user_privileges_test.py @@ -0,0 +1,25 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.azure.services.entra.entra_service import User +from prowler.providers.azure.services.entra.lib.user_privileges import ( + is_privileged_user, +) + + +class Test_user_privileges_test: + def test_user_in_privileged_roles(self): + user_id = str(uuid4()) + privileged_roles = {"admin": mock.MagicMock()} + privileged_roles["admin"].members = [User(id=user_id, name="user1")] + + user = User(id=user_id, name="user1") + assert is_privileged_user(user, privileged_roles) + + def test_user_not_in_privileged_roles(self): + user_id = str(uuid4()) + privileged_roles = {"admin": mock.MagicMock()} + privileged_roles["admin"].members = [User(id=str(uuid4()), name="user2")] + + user = User(id=user_id, name="user1") + assert not is_privileged_user(user, privileged_roles) From a18b79a3dcaf03178ecd2d8578418aa5adc38e3c Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Mon, 2 Dec 2024 12:34:31 +0100 Subject: [PATCH 11/44] feat(tests): Fix some microsoft tests, still not working --- .../fixtures/microsoft365_mutelist.yaml | 2 +- .../mutelist/microsoft365_mutelist_test.py | 4 +++- .../microsoft365_provider_test.py | 21 ------------------- 3 files changed, 4 insertions(+), 23 deletions(-) diff --git a/tests/providers/microsoft365/lib/mutelist/fixtures/microsoft365_mutelist.yaml b/tests/providers/microsoft365/lib/mutelist/fixtures/microsoft365_mutelist.yaml index 69fb405006f..07c2b38b4ca 100644 --- a/tests/providers/microsoft365/lib/mutelist/fixtures/microsoft365_mutelist.yaml +++ b/tests/providers/microsoft365/lib/mutelist/fixtures/microsoft365_mutelist.yaml @@ -8,7 +8,7 @@ Mutelist: Accounts: "subscription_1": Checks: - "aks_cluster_rbac_enabled": + "admincenter_users_between_two_and_four_global_admins": Regions: - "*" Resources: diff --git a/tests/providers/microsoft365/lib/mutelist/microsoft365_mutelist_test.py b/tests/providers/microsoft365/lib/mutelist/microsoft365_mutelist_test.py index b2572979555..ca828df7b58 100644 --- a/tests/providers/microsoft365/lib/mutelist/microsoft365_mutelist_test.py +++ b/tests/providers/microsoft365/lib/mutelist/microsoft365_mutelist_test.py @@ -5,7 +5,7 @@ from tests.lib.outputs.fixtures.fixtures import generate_finding_output MUTELIST_FIXTURE_PATH = ( - "tests/providers/microsoft365/lib/mutelist/fixtures/azure_mutelist.yaml" + "tests/providers/microsoft365/lib/mutelist/fixtures/microsoft365_mutelist.yaml" ) @@ -58,10 +58,12 @@ def test_is_finding_muted(self): mutelist = Microsoft365Mutelist(mutelist_content=mutelist_content) finding = MagicMock + finding.tenant_id = "subscription_1" finding.check_metadata = MagicMock finding.check_metadata.CheckID = "check_test" finding.status = "FAIL" finding.resource_name = "test_resource" + finding.tenant_domain = "test_domain" finding.resource_tags = [] assert mutelist.is_finding_muted(finding) diff --git a/tests/providers/microsoft365/microsoft365_provider_test.py b/tests/providers/microsoft365/microsoft365_provider_test.py index 6ed01f1b4c3..c5984ba5bfb 100644 --- a/tests/providers/microsoft365/microsoft365_provider_test.py +++ b/tests/providers/microsoft365/microsoft365_provider_test.py @@ -3,7 +3,6 @@ import pytest from azure.core.credentials import AccessToken -from azure.identity import DefaultAzureCredential from mock import MagicMock from prowler.config.config import ( @@ -54,19 +53,12 @@ def test_microsoft365_provider(self): base_url="https://management.azure.com", credential_scopes=["https://management.azure.com/.default"], ) - assert isinstance(microsoft365_provider.session, DefaultAzureCredential) assert microsoft365_provider.identity == Microsoft365IdentityInfo( identity_id="", identity_type="", tenant_id="", tenant_domain="Unknown tenant domain (missing AAD permissions)", ) - assert microsoft365_provider.audit_config == { - "shodan_api_key": None, - "php_latest_version": "8.2", - "python_latest_version": "3.12", - "java_latest_version": "17", - } def test_test_connection_tenant_id_client_id_client_secret(self): with patch( @@ -74,8 +66,6 @@ def test_test_connection_tenant_id_client_id_client_secret(self): ) as mock_default_credential, patch( "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" ) as mock_setup_session, patch( - "prowler.providers.microsoft365.microsoft365_provider.SubscriptionClient" - ) as mock_resource_client, patch( "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.validate_static_credentials" ) as mock_validate_static_credentials: @@ -97,12 +87,7 @@ def test_test_connection_tenant_id_client_id_client_secret(self): # Mock ValidateStaticCredentials to avoid real API calls mock_validate_static_credentials.return_value = None - # Mock ResourceManagementClient to avoid real API calls - mock_client = MagicMock() - mock_resource_client.return_value = mock_client - test_connection = Microsoft365Provider.test_connection( - browser_auth=False, tenant_id=str(uuid4()), region="AzureCloud", raise_on_exception=False, @@ -120,8 +105,6 @@ def test_test_connection_provider_validation(self): ) as mock_default_credential, patch( "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" ) as mock_setup_session, patch( - "prowler.providers.microsoft365.microsoft365_provider.SubscriptionClient" - ) as mock_resource_client, patch( "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.validate_static_credentials" ) as mock_validate_static_credentials: @@ -142,12 +125,8 @@ def test_test_connection_provider_validation(self): # Mock ResourceManagementClient to avoid real API calls mock_subscription = MagicMock() mock_subscription.subscription_id = "test_provider_id" - mock_return_value = MagicMock() - mock_return_value.subscriptions.list.return_value = [mock_subscription] - mock_resource_client.return_value = mock_return_value test_connection = Microsoft365Provider.test_connection( - browser_auth=False, tenant_id=str(uuid4()), region="AzureCloud", raise_on_exception=False, From 68e29b6683fbde876525b36f3bfa01dd9f307e0d Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Mon, 2 Dec 2024 12:36:55 +0100 Subject: [PATCH 12/44] feat(microsoft365): Fix exceptions, make some changes in provider to test connection, not working --- prowler/lib/check/models.py | 2 + prowler/lib/outputs/finding.py | 2 +- .../microsoft365/exceptions/exceptions.py | 161 ++++----------- .../microsoft365/lib/mutelist/mutelist.py | 5 +- .../microsoft365/microsoft365_provider.py | 193 ++++++++++-------- prowler/providers/microsoft365/models.py | 1 + 6 files changed, 157 insertions(+), 207 deletions(-) diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index ceb345f814f..6e1696b8275 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -490,6 +490,7 @@ class Check_Report_Microsoft365(Check_Report): resource_name: str resource_id: str tenant_id: str + tenant_domain: str location: str def __init__(self, metadata): @@ -497,6 +498,7 @@ def __init__(self, metadata): self.resource_name = "" self.resource_id = "" self.tenant_id = "" + self.tenant_domain = "" self.location = "global" diff --git a/prowler/lib/outputs/finding.py b/prowler/lib/outputs/finding.py index da05e75844d..c2809379a97 100644 --- a/prowler/lib/outputs/finding.py +++ b/prowler/lib/outputs/finding.py @@ -237,7 +237,7 @@ def generate_output( f"{provider.identity.identity_type}: {provider.identity.identity_id}" ) output_data["account_uid"] = get_nested_attribute( - provider, "identity.tenant_domain" + provider, "identity.tenant_id" ) output_data["account_name"] = get_nested_attribute( provider, "identity.tenant_domain" diff --git a/prowler/providers/microsoft365/exceptions/exceptions.py b/prowler/providers/microsoft365/exceptions/exceptions.py index cac4ca4729b..53c31fda5d8 100644 --- a/prowler/providers/microsoft365/exceptions/exceptions.py +++ b/prowler/providers/microsoft365/exceptions/exceptions.py @@ -10,90 +10,70 @@ class Microsoft365BaseException(ProwlerException): "message": "Microsoft365 environment variable error", "remediation": "Check the Microsoft365 environment variables and ensure they are properly set.", }, - (6001, "Microsoft365NoSubscriptionsError"): { - "message": "No Microsoft365 subscriptions found", - "remediation": "Check the Microsoft365 subscriptions and ensure they are properly set up.", - }, - (6002, "Microsoft365SetUpIdentityError"): { - "message": "Microsoft365 identity setup error related with credentials", - "remediation": "Check credentials and ensure they are properly set up for Microsoft365 and the identity provider.", - }, - (6003, "Microsoft365NoAuthenticationMethodError"): { - "message": "No Microsoft365 authentication method found", - "remediation": "Check that any authentication method is properly set up for Microsoft365.", - }, - (6006, "Microsoft365ArgumentTypeValidationError"): { + (6001, "Microsoft365ArgumentTypeValidationError"): { "message": "Microsoft365 argument type validation error", "remediation": "Check the provided argument types specific to Microsoft365 and ensure they meet the required format.", }, - (6007, "Microsoft365SetUpRegionConfigError"): { + (6002, "Microsoft365SetUpRegionConfigError"): { "message": "Microsoft365 region configuration setup error", "remediation": "Check the Microsoft365 region configuration and ensure it is properly set up.", }, - (6008, "Microsoft365DefaultMicrosoft365CredentialError"): { - "message": "Error in DefaultMicrosoft365Credential", - "remediation": "Check that all the attributes are properly set up for the DefaultMicrosoft365Credential.", - }, - (6009, "Microsoft365InteractiveBrowserCredentialError"): { - "message": "Error retrieving InteractiveBrowserCredential", - "remediation": "Check your browser and ensure that the tenant ID and browser authentication are properly set.", + (6003, "Microsoft365DefaultAzureCredentialError"): { + "message": "Error in DefaultAzureCredential", + "remediation": "Check that all the attributes are properly set up for the DefaultAzureCredential.", }, - (6010, "Microsoft365HTTPResponseError"): { + (6004, "Microsoft365HTTPResponseError"): { "message": "Error in HTTP response from Microsoft365", "remediation": "", }, - (6011, "Microsoft365CredentialsUnavailableError"): { + (6005, "Microsoft365CredentialsUnavailableError"): { "message": "Error trying to configure Microsoft365 credentials because they are unavailable", "remediation": "Check the dictionary and ensure it is properly set up for Microsoft365 credentials. TENANT_ID, CLIENT_ID and CLIENT_SECRET are required.", }, - (6012, "Microsoft365GetTokenIdentityError"): { + (6006, "Microsoft365GetTokenIdentityError"): { "message": "Error trying to get token from Microsoft365 Identity", "remediation": "Check the Microsoft365 Identity and ensure it is properly set up.", }, - (6013, "Microsoft365NotTenantIdButClientIdAndClienSecretError"): { - "message": "The provided credentials are not a tenant ID but a client ID and client secret", - "remediation": "Tenant Id, Client Id and Client Secret are required for Microsoft365 credentials. Make sure you are using the correct credentials.", - }, - (6014, "Microsoft365ClientAuthenticationError"): { + (6007, "Microsoft365ClientAuthenticationError"): { "message": "Error in client authentication", "remediation": "Check the client authentication and ensure it is properly set up.", }, - (6015, "Microsoft365SetUpSessionError"): { - "message": "Error setting up session", - "remediation": "Check the session setup and ensure it is properly set up.", - }, - (6016, "Microsoft365NotValidTenantIdError"): { + (6008, "Microsoft365NotValidTenantIdError"): { "message": "The provided tenant ID is not valid", "remediation": "Check the tenant ID and ensure it is a valid ID.", }, - (6017, "Microsoft365NotValidClientIdError"): { + (6009, "Microsoft365NotValidClientIdError"): { "message": "The provided client ID is not valid", "remediation": "Check the client ID and ensure it is a valid ID.", }, - (6018, "Microsoft365NotValidClientSecretError"): { + (6010, "Microsoft365NotValidClientSecretError"): { "message": "The provided client secret is not valid", "remediation": "Check the client secret and ensure it is a valid secret.", }, - (6019, "Microsoft365ConfigCredentialsError"): { + (6011, "Microsoft365ConfigCredentialsError"): { "message": "Error in configuration of Microsoft365 credentials", "remediation": "Check the configuration of Microsoft365 credentials and ensure it is properly set up.", }, - (6020, "Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError"): { + (6012, "Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError"): { "message": "The provided client ID and client secret do not belong to the provided tenant ID", "remediation": "Check the client ID and client secret and ensure they belong to the provided tenant ID.", }, - (6021, "Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError"): { + (6013, "Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError"): { "message": "The provided tenant ID and client secret do not belong to the provided client ID", "remediation": "Check the tenant ID and client secret and ensure they belong to the provided client ID.", }, - (6022, "Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError"): { + (6014, "Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError"): { "message": "The provided tenant ID and client ID do not belong to the provided client secret", "remediation": "Check the tenant ID and client ID and ensure they belong to the provided client secret.", }, - (6023, "Microsoft365InvalidProviderIdError"): { + (6015, "Microsoft365InvalidProviderIdError"): { "message": "The provided provider_id does not match with the available subscriptions", "remediation": "Check the provider_id and ensure it is a valid subscription for the given credentials.", }, + (6016, "Microsoft365NoAuthenticationMethodError"): { + "message": "No Azure authentication method found", + "remediation": "Check that any authentication method is properly set up for Azure.", + }, } def __init__(self, code, file=None, original_exception=None, message=None): @@ -120,142 +100,84 @@ def __init__(self, code, file=None, original_exception=None, message=None): class Microsoft365EnvironmentVariableError(Microsoft365CredentialsError): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 2000, file=file, original_exception=original_exception, message=message - ) - - -class Microsoft365NoSubscriptionsError(Microsoft365CredentialsError): - def __init__(self, file=None, original_exception=None, message=None): - super().__init__( - 2001, file=file, original_exception=original_exception, message=message - ) - - -class Microsoft365SetUpIdentityError(Microsoft365CredentialsError): - def __init__(self, file=None, original_exception=None, message=None): - super().__init__( - 2002, file=file, original_exception=original_exception, message=message - ) - - -class Microsoft365NoAuthenticationMethodError(Microsoft365CredentialsError): - def __init__(self, file=None, original_exception=None, message=None): - super().__init__( - 2003, file=file, original_exception=original_exception, message=message - ) - - -class Microsoft365BrowserAuthNoTenantIDError(Microsoft365CredentialsError): - def __init__(self, file=None, original_exception=None, message=None): - super().__init__( - 2004, file=file, original_exception=original_exception, message=message - ) - - -class Microsoft365TenantIDNoBrowserAuthError(Microsoft365CredentialsError): - def __init__(self, file=None, original_exception=None, message=None): - super().__init__( - 2005, file=file, original_exception=original_exception, message=message + 6000, file=file, original_exception=original_exception, message=message ) class Microsoft365ArgumentTypeValidationError(Microsoft365BaseException): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 2006, file=file, original_exception=original_exception, message=message + 6001, file=file, original_exception=original_exception, message=message ) class Microsoft365SetUpRegionConfigError(Microsoft365BaseException): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 2007, file=file, original_exception=original_exception, message=message - ) - - -class Microsoft365DefaultMicrosoft365CredentialError(Microsoft365CredentialsError): - def __init__(self, file=None, original_exception=None, message=None): - super().__init__( - 2008, file=file, original_exception=original_exception, message=message + 6003, file=file, original_exception=original_exception, message=message ) -class Microsoft365InteractiveBrowserCredentialError(Microsoft365CredentialsError): +class Microsoft365DefaultAzureCredentialError(Microsoft365CredentialsError): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 2009, file=file, original_exception=original_exception, message=message + 6003, file=file, original_exception=original_exception, message=message ) class Microsoft365HTTPResponseError(Microsoft365BaseException): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 2010, file=file, original_exception=original_exception, message=message + 6004, file=file, original_exception=original_exception, message=message ) class Microsoft365CredentialsUnavailableError(Microsoft365CredentialsError): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 2011, file=file, original_exception=original_exception, message=message + 6005, file=file, original_exception=original_exception, message=message ) class Microsoft365GetTokenIdentityError(Microsoft365BaseException): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 2012, file=file, original_exception=original_exception, message=message - ) - - -class Microsoft365NotTenantIdButClientIdAndClienSecretError( - Microsoft365CredentialsError -): - def __init__(self, file=None, original_exception=None, message=None): - super().__init__( - 2013, file=file, original_exception=original_exception, message=message + 6006, file=file, original_exception=original_exception, message=message ) class Microsoft365ClientAuthenticationError(Microsoft365CredentialsError): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 2014, file=file, original_exception=original_exception, message=message - ) - - -class Microsoft365SetUpSessionError(Microsoft365CredentialsError): - def __init__(self, file=None, original_exception=None, message=None): - super().__init__( - 2015, file=file, original_exception=original_exception, message=message + 6007, file=file, original_exception=original_exception, message=message ) class Microsoft365NotValidTenantIdError(Microsoft365CredentialsError): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 2016, file=file, original_exception=original_exception, message=message + 6008, file=file, original_exception=original_exception, message=message ) class Microsoft365NotValidClientIdError(Microsoft365CredentialsError): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 2017, file=file, original_exception=original_exception, message=message + 6009, file=file, original_exception=original_exception, message=message ) class Microsoft365NotValidClientSecretError(Microsoft365CredentialsError): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 2018, file=file, original_exception=original_exception, message=message + 6010, file=file, original_exception=original_exception, message=message ) class Microsoft365ConfigCredentialsError(Microsoft365CredentialsError): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 2019, file=file, original_exception=original_exception, message=message + 6011, file=file, original_exception=original_exception, message=message ) @@ -264,7 +186,7 @@ class Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError( ): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 2020, file=file, original_exception=original_exception, message=message + 6012, file=file, original_exception=original_exception, message=message ) @@ -273,7 +195,7 @@ class Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError( ): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 2021, file=file, original_exception=original_exception, message=message + 6013, file=file, original_exception=original_exception, message=message ) @@ -282,12 +204,19 @@ class Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError( ): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 2022, file=file, original_exception=original_exception, message=message + 6014, file=file, original_exception=original_exception, message=message ) class Microsoft365InvalidProviderIdError(Microsoft365BaseException): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 2023, file=file, original_exception=original_exception, message=message + 6015, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365NoAuthenticationMethodError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6016, file=file, original_exception=original_exception, message=message ) diff --git a/prowler/providers/microsoft365/lib/mutelist/mutelist.py b/prowler/providers/microsoft365/lib/mutelist/mutelist.py index 85b61fc06da..0e5a82951a3 100644 --- a/prowler/providers/microsoft365/lib/mutelist/mutelist.py +++ b/prowler/providers/microsoft365/lib/mutelist/mutelist.py @@ -7,11 +7,12 @@ class Microsoft365Mutelist(Mutelist): def is_finding_muted( self, finding: Check_Report_Microsoft365, - cluster: str, ) -> bool: return self.is_muted( - cluster, + finding.tenant_id, finding.check_metadata.CheckID, + finding.tenant_domain, + finding.location, finding.resource_name, unroll_dict(unroll_tags(finding.resource_tags)), ) diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index f4882701411..f6d1cc8f5e2 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -5,10 +5,10 @@ from os import getenv from uuid import UUID -import requests from azure.core.exceptions import ClientAuthenticationError, HttpResponseError from azure.identity import ClientSecretCredential, DefaultAzureCredential from colorama import Fore, Style +from msal import ConfidentialClientApplication from msgraph import GraphServiceClient from prowler.config.config import ( @@ -26,11 +26,10 @@ Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError, Microsoft365ConfigCredentialsError, Microsoft365CredentialsUnavailableError, - Microsoft365DefaultMicrosoft365CredentialError, + Microsoft365DefaultAzureCredentialError, Microsoft365EnvironmentVariableError, Microsoft365GetTokenIdentityError, Microsoft365HTTPResponseError, - Microsoft365InteractiveBrowserCredentialError, Microsoft365InvalidProviderIdError, Microsoft365NotValidClientIdError, Microsoft365NotValidClientSecretError, @@ -107,7 +106,6 @@ def __init__( Initializes the Microsoft365 provider. Args: - app_env_auth (bool): Flag indicating whether to use application authentication with environment variables. tenant_id (str): The Microsoft365 Active Directory tenant ID. region (str): The Microsoft365 region. client_id (str): The Microsoft365 client ID. @@ -124,8 +122,7 @@ def __init__( Raises: Microsoft365ArgumentTypeValidationError: If there is an error in the argument type validation. Microsoft365SetUpRegionConfigError: If there is an error in setting up the region configuration. - Microsoft365DefaultMicrosoft365CredentialError: If there is an error in retrieving the Microsoft365 credentials. - Microsoft365InteractiveBrowserCredentialError: If there is an error in retrieving the Microsoft365 credentials using browser authentication. + Microsoft365DefaultAzureCredentialError: If there is an error in retrieving the Microsoft365 credentials. Microsoft365ConfigCredentialsError: If there is an error in configuring the Microsoft365 credentials from a dictionary. Microsoft365GetTokenIdentityError: If there is an error in getting the token from the Microsoft365 identity. Microsoft365HTTPResponseError: If there is an HTTP response error. @@ -135,8 +132,17 @@ def __init__( logger.info("Checking if region is different than default one") self._region_config = self.setup_region_config(region) + # Get the dict from the static credentials + microsoft365_credentials = None + if tenant_id and client_id and client_secret: + microsoft365_credentials = self.validate_static_credentials( + tenant_id=tenant_id, client_id=client_id, client_secret=client_secret + ) + # Set up the Microsoft365 session - self._session = self.setup_session() + self._session = self.setup_session( + microsoft365_credentials, + ) # Set up the identity self._identity = self.setup_identity() @@ -201,6 +207,30 @@ def mutelist(self) -> Microsoft365Mutelist: """Mutelist object associated with this Microsoft365 provider.""" return self._mutelist + @staticmethod + def validate_arguments( + tenant_id: str, + client_id: str, + client_secret: str, + ): + """ + Validates the authentication arguments for the Azure provider. + + Args: + tenant_id (str): The Azure Tenant ID. + client_id (str): The Azure Client ID. + client_secret (str): The Azure Client Secret. + + Raises: + AzureBrowserAuthNoTenantIDError: If browser authentication is enabled but the tenant ID is not found. + """ + + if not client_id or not client_secret or not tenant_id: + raise Microsoft365IdentityInfo( + file=os.path.basename(__file__), + message="Tenant Id is required for Azure static credentials. Make sure you are using the correct credentials.", + ) + @staticmethod def setup_region_config(region): """ @@ -254,6 +284,7 @@ def print_credentials(self): """ report_lines = [ f"Microsoft365 Region: {Fore.YELLOW}{self.region_config.name}{Style.RESET_ALL}", + f"Microsoft365 Tenant Domain: {Fore.YELLOW}{self.identity.tenant_domain}{Style.RESET_ALL} Microsoft365 Tenant ID: {Fore.YELLOW}{self._identity.tenant_id}{Style.RESET_ALL}", f"Microsoft365 Identity Type: {Fore.YELLOW}{self._identity.identity_type}{Style.RESET_ALL} Microsoft365 Identity ID: {Fore.YELLOW}{self._identity.identity_id}{Style.RESET_ALL}", ] report_title = ( @@ -264,7 +295,9 @@ def print_credentials(self): # TODO: setup_session or setup_credentials? # This should be setup_credentials, since it is setting up the credentials for the provider @staticmethod - def setup_session(): + def setup_session( + microsoft365_credentials: dict, + ): """Returns the Microsoft365 credentials object. Set up the Microsoft365 session with the specified authentication method. @@ -305,54 +338,48 @@ def test_connection( raise_on_exception=True, client_id=None, client_secret=None, - provider_id=None, ) -> Connection: - """Test connection to Azure subscription. + """Test connection to Microsoft365 subscription. - Test the connection to an Azure subscription using the provided credentials. + Test the connection to an Microsoft365 subscription using the provided credentials. Args: - tenant_id (str): The Azure Active Directory tenant ID. - region (str): The Azure region. + tenant_id (str): The Microsoft365 Active Directory tenant ID. + region (str): The Microsoft365 region. raise_on_exception (bool): Flag indicating whether to raise an exception if the connection fails. - client_id (str): The Azure client ID. - client_secret (str): The Azure client secret. - provider_id (str): The provider ID, in this case it's the Azure subscription ID. + client_id (str): The Microsoft365 client ID. + client_secret (str): The Microsoft365 client secret. + provider_id (str): The provider ID, in this case it's the Microsoft365 subscription ID. Returns: bool: True if the connection is successful, False otherwise. Raises: - Exception: If failed to test the connection to Azure subscription. - AzureArgumentTypeValidationError: If there is an error in the argument type validation. - AzureSetUpRegionConfigError: If there is an error in setting up the region configuration. - AzureDefaultAzureCredentialError: If there is an error in retrieving the Azure credentials. - AzureInteractiveBrowserCredentialError: If there is an error in retrieving the Azure credentials using browser authentication. - AzureHTTPResponseError: If there is an HTTP response error. - AzureConfigCredentialsError: If there is an error in configuring the Azure credentials from a dictionary. + Exception: If failed to test the connection to Microsoft365 subscription. + Microsoft365ArgumentTypeValidationError: If there is an error in the argument type validation. + Microsoft365SetUpRegionConfigError: If there is an error in setting up the region configuration. + Microsoft365DefaultAzureCredentialError: If there is an error in retrieving the Microsoft365 credentials. + Microsoft365InteractiveBrowserCredentialError: If there is an error in retrieving the Microsoft365 credentials using browser authentication. + Microsoft365HTTPResponseError: If there is an HTTP response error. + Microsoft365ConfigCredentialsError: If there is an error in configuring the Microsoft365 credentials from a dictionary. Examples: - >>> AzureProvider.test_connection(az_cli_auth=True) + >>> Microsoft365Provider.test_connection(az_cli_auth=True) True - >>> AzureProvider.test_connection(sp_env_auth=False, browser_auth=True, tenant_id=None) - False, ArgumentTypeError: Azure Tenant ID is required only for browser authentication mode - >>> AzureProvider.test_connection(tenant_id="XXXXXXXXXX", client_id="XXXXXXXXXX", client_secret="XXXXXXXXXX") + >>> Microsoft365Provider.test_connection(sp_env_auth=False, browser_auth=True, tenant_id=None) + False, ArgumentTypeError: Microsoft365 Tenant ID is required only for browser authentication mode + >>> Microsoft365Provider.test_connection(tenant_id="XXXXXXXXXX", client_id="XXXXXXXXXX", client_secret="XXXXXXXXXX") True """ try: - Microsoft365Provider.validate_arguments( - tenant_id, - client_id, - client_secret, - ) - region_config = Microsoft365Provider.setup_region_config(region) + Microsoft365Provider.setup_region_config(region) # Get the dict from the static credentials - Microsoft365_credentials = None + microsoft365_credentials = None if tenant_id and client_id and client_secret: - Microsoft365_credentials = ( + microsoft365_credentials = ( Microsoft365Provider.validate_static_credentials( tenant_id=tenant_id, client_id=client_id, @@ -362,14 +389,10 @@ def test_connection( # Set up the Microsoft365 session Microsoft365Provider.setup_session( - tenant_id, - Microsoft365_credentials, - region_config, + microsoft365_credentials, ) - logger.info( - "Microsoft365 provider: Connection to Microsoft365 subscription successful" - ) + logger.info("Microsoft365 provider: Connection to Microsoft365 successful") return Connection(is_connected=True) @@ -396,24 +419,13 @@ def test_connection( if raise_on_exception: raise environment_credentials_error return Connection(error=environment_credentials_error) - except ( - Microsoft365DefaultMicrosoft365CredentialError - ) as default_credentials_error: + except Microsoft365DefaultAzureCredentialError as default_credentials_error: logger.error( f"{default_credentials_error.__class__.__name__}[{default_credentials_error.__traceback__.tb_lineno}]: {default_credentials_error}" ) if raise_on_exception: raise default_credentials_error return Connection(error=default_credentials_error) - except ( - Microsoft365InteractiveBrowserCredentialError - ) as interactive_browser_error: - logger.error( - f"{interactive_browser_error.__class__.__name__}[{interactive_browser_error.__traceback__.tb_lineno}]: {interactive_browser_error}" - ) - if raise_on_exception: - raise interactive_browser_error - return Connection(error=interactive_browser_error) except Microsoft365ConfigCredentialsError as config_credentials_error: logger.error( f"{config_credentials_error.__class__.__name__}[{config_credentials_error.__traceback__.tb_lineno}]: {config_credentials_error}" @@ -435,9 +447,7 @@ def test_connection( if raise_on_exception: raise credential_unavailable_error return Connection(error=credential_unavailable_error) - except ( - Microsoft365DefaultMicrosoft365CredentialError - ) as default_credentials_error: + except Microsoft365DefaultAzureCredentialError as default_credentials_error: logger.error( f"{default_credentials_error.__class__.__name__}[{default_credentials_error.__traceback__.tb_lineno}]: {default_credentials_error}" ) @@ -531,7 +541,7 @@ def setup_identity( Sets up the identity for the Microsoft365 provider. Args: - app_env_auth (bool): Flag indicating if Service Principal environment authentication is used. + None Returns: Microsoft365IdentityInfo: An instance of Microsoft365IdentityInfo containing the identity information. @@ -582,6 +592,7 @@ async def get_microsoft365_identity(): # The id of the sp can be retrieved from environment variables identity.identity_id = getenv("APP_CLIENT_ID") identity.identity_type = "Application" + identity.tenant_id = getenv("APP_TENANT_ID") asyncio.get_event_loop().run_until_complete(get_microsoft365_identity()) @@ -684,34 +695,40 @@ def verify_client(tenant_id, client_id, client_secret) -> None: Returns: None """ - url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token" - headers = { - "Content-Type": "application/x-www-form-urlencoded", - "Accept": "application/json", - } - data = { - "grant_type": "client_credentials", - "client_id": client_id, - "client_secret": client_secret, - "scope": "https://graph.microsoft.com/.default", - } - response = requests.post(url, headers=headers, data=data).json() - if "access_token" not in response.keys() and "error_codes" in response.keys(): - if f"Tenant '{tenant_id}'" in response["error_description"]: - raise Microsoft365NotValidTenantIdError( - file=os.path.basename(__file__), - message="The provided Microsoft 365 Tenant ID is not valid for the specified Client ID and Client Secret.", - ) - if ( - f"Application with identifier '{client_id}'" - in response["error_description"] - ): - raise Microsoft365NotValidClientIdError( - file=os.path.basename(__file__), - message="The provided Microsoft 365 Client ID is not valid for the specified Tenant ID and Client Secret.", - ) - if "Invalid client secret provided" in response["error_description"]: - raise Microsoft365NotValidClientSecretError( - file=os.path.basename(__file__), - message="The provided Microsoft 365 Client Secret is not valid for the specified Tenant ID and Client ID.", - ) + authority = f"https://login.microsoftonline.com/{tenant_id}" + try: + # Create a ConfidentialClientApplication instance + app = ConfidentialClientApplication( + client_id=client_id, + client_credential=client_secret, + authority=authority, + ) + + # Attempt to acquire a token + result = app.acquire_token_for_client( + scopes=["https://graph.microsoft.com/.default"] + ) + + # Check if token acquisition was successful + if "access_token" not in result: + # Handle specific errors based on the MSAL response + error_description = result.get("error_description", "") + if f"Tenant '{tenant_id}'" in error_description: + raise Microsoft365NotValidTenantIdError( + file=os.path.basename(__file__), + message="The provided Microsoft 365 Tenant ID is not valid for the specified Client ID and Client Secret.", + ) + if f"Application with identifier '{client_id}'" in error_description: + raise Microsoft365NotValidClientIdError( + file=os.path.basename(__file__), + message="The provided Microsoft 365 Client ID is not valid for the specified Tenant ID and Client Secret.", + ) + if "Invalid client secret provided" in error_description: + raise Microsoft365NotValidClientSecretError( + file=os.path.basename(__file__), + message="The provided Microsoft 365 Client Secret is not valid for the specified Tenant ID and Client ID.", + ) + + except Exception as e: + # Generic exception handling (if needed) + raise RuntimeError(f"An unexpected error occurred: {str(e)}") diff --git a/prowler/providers/microsoft365/models.py b/prowler/providers/microsoft365/models.py index 9e948eaae5c..c1e028e8d6c 100644 --- a/prowler/providers/microsoft365/models.py +++ b/prowler/providers/microsoft365/models.py @@ -9,6 +9,7 @@ class Microsoft365IdentityInfo(BaseModel): identity_type: str = "" tenant_id: str = "" tenant_domain: str = "Unknown tenant domain (missing AAD permissions)" + location: str = "" class Microsoft365RegionConfig(BaseModel): From ac80388c546ac5ee0f3d2e2aec5deee73965dd5b Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Tue, 3 Dec 2024 13:05:58 +0100 Subject: [PATCH 13/44] feat(microsoft365): Finish provider and mutelist tests --- .../mutelist/microsoft365_mutelist_test.py | 1 + .../microsoft365_provider_test.py | 168 +----------------- 2 files changed, 6 insertions(+), 163 deletions(-) diff --git a/tests/providers/microsoft365/lib/mutelist/microsoft365_mutelist_test.py b/tests/providers/microsoft365/lib/mutelist/microsoft365_mutelist_test.py index ca828df7b58..5bbde2e89d9 100644 --- a/tests/providers/microsoft365/lib/mutelist/microsoft365_mutelist_test.py +++ b/tests/providers/microsoft365/lib/mutelist/microsoft365_mutelist_test.py @@ -62,6 +62,7 @@ def test_is_finding_muted(self): finding.check_metadata = MagicMock finding.check_metadata.CheckID = "check_test" finding.status = "FAIL" + finding.location = "global" finding.resource_name = "test_resource" finding.tenant_domain = "test_domain" finding.resource_tags = [] diff --git a/tests/providers/microsoft365/microsoft365_provider_test.py b/tests/providers/microsoft365/microsoft365_provider_test.py index c5984ba5bfb..5c951f16fdb 100644 --- a/tests/providers/microsoft365/microsoft365_provider_test.py +++ b/tests/providers/microsoft365/microsoft365_provider_test.py @@ -2,7 +2,6 @@ from uuid import uuid4 import pytest -from azure.core.credentials import AccessToken from mock import MagicMock from prowler.config.config import ( @@ -11,11 +10,6 @@ load_and_validate_config_file, ) from prowler.providers.common.models import Connection -from prowler.providers.microsoft365.exceptions.exceptions import ( - Microsoft365HTTPResponseError, - Microsoft365InvalidProviderIdError, - Microsoft365NoAuthenticationMethodError, -) from prowler.providers.microsoft365.microsoft365_provider import Microsoft365Provider from prowler.providers.microsoft365.models import ( Microsoft365IdentityInfo, @@ -32,7 +26,7 @@ def test_microsoft365_provider(self): fixer_config = load_and_validate_config_file( "microsoft365", default_fixer_config_file_path ) - azure_region = "AzureCloud" + azure_region = "Microsoft365Global" with patch( "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_identity", @@ -48,10 +42,10 @@ def test_microsoft365_provider(self): ) assert microsoft365_provider.region_config == Microsoft365RegionConfig( - name="AzureCloud", + name="Microsoft365Global", authority=None, - base_url="https://management.azure.com", - credential_scopes=["https://management.azure.com/.default"], + base_url="https://graph.microsoft.com", + credential_scopes=["https://graph.microsoft.com/.default"], ) assert microsoft365_provider.identity == Microsoft365IdentityInfo( identity_id="", @@ -62,59 +56,11 @@ def test_microsoft365_provider(self): def test_test_connection_tenant_id_client_id_client_secret(self): with patch( - "prowler.providers.microsoft365.microsoft365_provider.DefaultAzureCredential" - ) as mock_default_credential, patch( - "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" - ) as mock_setup_session, patch( - "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.validate_static_credentials" - ) as mock_validate_static_credentials: - - # Mock the return value of DefaultAzureCredential - mock_credentials = MagicMock() - mock_credentials.get_token.return_value = AccessToken( - token="fake_token", expires_on=9999999999 - ) - mock_default_credential.return_value = { - "client_id": str(uuid4()), - "client_secret": str(uuid4()), - "tenant_id": str(uuid4()), - } - - # Mock setup_session to return a mocked session object - mock_session = MagicMock() - mock_setup_session.return_value = mock_session - - # Mock ValidateStaticCredentials to avoid real API calls - mock_validate_static_credentials.return_value = None - - test_connection = Microsoft365Provider.test_connection( - tenant_id=str(uuid4()), - region="AzureCloud", - raise_on_exception=False, - client_id=str(uuid4()), - client_secret=str(uuid4()), - ) - - assert isinstance(test_connection, Connection) - assert test_connection.is_connected - assert test_connection.error is None - - def test_test_connection_provider_validation(self): - with patch( - "prowler.providers.microsoft365.microsoft365_provider.DefaultAzureCredential" - ) as mock_default_credential, patch( "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" ) as mock_setup_session, patch( "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.validate_static_credentials" ) as mock_validate_static_credentials: - # Mock the return value of DefaultAzureCredential - mock_default_credential.return_value = { - "client_id": str(uuid4()), - "client_secret": str(uuid4()), - "tenant_id": str(uuid4()), - } - # Mock setup_session to return a mocked session object mock_session = MagicMock() mock_setup_session.return_value = mock_session @@ -122,121 +68,18 @@ def test_test_connection_provider_validation(self): # Mock ValidateStaticCredentials to avoid real API calls mock_validate_static_credentials.return_value = None - # Mock ResourceManagementClient to avoid real API calls - mock_subscription = MagicMock() - mock_subscription.subscription_id = "test_provider_id" - test_connection = Microsoft365Provider.test_connection( tenant_id=str(uuid4()), - region="AzureCloud", + region="Microsoft365Global", raise_on_exception=False, client_id=str(uuid4()), client_secret=str(uuid4()), - provider_id="test_provider_id", ) assert isinstance(test_connection, Connection) assert test_connection.is_connected assert test_connection.error is None - def test_test_connection_provider_validation_error(self): - with patch( - "prowler.providers.microsoft365.microsoft365_provider.DefaultAzureCredential" - ) as mock_default_credential, patch( - "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" - ) as mock_setup_session, patch( - "prowler.providers.microsoft365.microsoft365_provider.SubscriptionClient" - ) as mock_resource_client, patch( - "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.validate_static_credentials" - ) as mock_validate_static_credentials: - - # Mock the return value of DefaultAzureCredential - mock_default_credential.return_value = { - "client_id": str(uuid4()), - "client_secret": str(uuid4()), - "tenant_id": str(uuid4()), - } - - # Mock setup_session to return a mocked session object - mock_session = MagicMock() - mock_setup_session.return_value = mock_session - - # Mock ValidateStaticCredentials to avoid real API calls - mock_validate_static_credentials.return_value = None - - # Mock ResourceManagementClient to avoid real API calls - mock_subscription = MagicMock() - mock_subscription.subscription_id = "test_invalid_provider_id" - mock_return_value = MagicMock() - mock_return_value.subscriptions.list.return_value = [mock_subscription] - mock_resource_client.return_value = mock_return_value - - test_connection = Microsoft365Provider.test_connection( - browser_auth=False, - tenant_id=str(uuid4()), - region="AzureCloud", - raise_on_exception=False, - client_id=str(uuid4()), - client_secret=str(uuid4()), - provider_id="test_provider_id", - ) - - assert test_connection.error is not None - assert isinstance(test_connection.error, Microsoft365InvalidProviderIdError) - assert ( - "The provided credentials are not valid for the specified Microsoft365 subscription." - in test_connection.error.args[0] - ) - - def test_test_connection_with_ClientAuthenticationError(self): - with pytest.raises(Microsoft365HTTPResponseError) as exception: - tenant_id = str(uuid4()) - Microsoft365Provider.test_connection( - browser_auth=True, - tenant_id=tenant_id, - region="AzureCloud", - ) - - assert exception.type == Microsoft365HTTPResponseError - assert ( - exception.value.args[0] - == f"[2010] Error in HTTP response from Microsoft365 - Authentication failed: Unable to get authority configuration for https://login.microsoftonline.com/{tenant_id}. Authority would typically be in a format of https://login.microsoftonline.com/your_tenant or https://tenant_name.ciamlogin.com or https://tenant_name.b2clogin.com/tenant.onmicrosoft.com/policy. Also please double check your tenant name or GUID is correct." - ) - - def test_test_connection_without_any_method(self): - with pytest.raises(Microsoft365NoAuthenticationMethodError) as exception: - Microsoft365Provider.test_connection() - - assert exception.type == Microsoft365NoAuthenticationMethodError - assert ( - "[2003] Microsoft365 provider requires at least one authentication method set: [--az-cli-auth | --sp-env-auth | --browser-auth | --managed-identity-auth]" - in exception.value.args[0] - ) - - def test_test_connection_with_httpresponseerror(self): - with patch( - "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.get_locations", - return_value={}, - ), patch( - "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" - ) as mock_setup_session: - - mock_setup_session.side_effect = Microsoft365HTTPResponseError( - file="test_file", original_exception="Simulated HttpResponseError" - ) - - with pytest.raises(Microsoft365HTTPResponseError) as exception: - Microsoft365Provider.test_connection( - az_cli_auth=True, - raise_on_exception=True, - ) - - assert exception.type == Microsoft365HTTPResponseError - assert ( - exception.value.args[0] - == "[2010] Error in HTTP response from Microsoft365 - Simulated HttpResponseError" - ) - def test_test_connection_with_exception(self): with patch( "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" @@ -246,7 +89,6 @@ def test_test_connection_with_exception(self): with pytest.raises(Exception) as exception: Microsoft365Provider.test_connection( - sp_env_auth=True, raise_on_exception=True, ) From 1dccb40a2de00803b2fbb6bbf3d31315e65085aa Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Tue, 3 Dec 2024 13:19:44 +0100 Subject: [PATCH 14/44] feat(microsoft365): Finish provider with test connection, finish exceptions, regions and mutelist --- .../microsoft365/exceptions/exceptions.py | 72 +++++++-------- .../microsoft365/lib/arguments/arguments.py | 22 ++--- .../microsoft365/lib/mutelist/mutelist.py | 1 - .../microsoft365/lib/regions/regions.py | 25 +++--- .../microsoft365/microsoft365_provider.py | 89 ++++++++++++------- 5 files changed, 109 insertions(+), 100 deletions(-) diff --git a/prowler/providers/microsoft365/exceptions/exceptions.py b/prowler/providers/microsoft365/exceptions/exceptions.py index 53c31fda5d8..8be172b7f81 100644 --- a/prowler/providers/microsoft365/exceptions/exceptions.py +++ b/prowler/providers/microsoft365/exceptions/exceptions.py @@ -18,61 +18,61 @@ class Microsoft365BaseException(ProwlerException): "message": "Microsoft365 region configuration setup error", "remediation": "Check the Microsoft365 region configuration and ensure it is properly set up.", }, - (6003, "Microsoft365DefaultAzureCredentialError"): { - "message": "Error in DefaultAzureCredential", - "remediation": "Check that all the attributes are properly set up for the DefaultAzureCredential.", - }, - (6004, "Microsoft365HTTPResponseError"): { + (6003, "Microsoft365HTTPResponseError"): { "message": "Error in HTTP response from Microsoft365", "remediation": "", }, - (6005, "Microsoft365CredentialsUnavailableError"): { + (6004, "Microsoft365CredentialsUnavailableError"): { "message": "Error trying to configure Microsoft365 credentials because they are unavailable", "remediation": "Check the dictionary and ensure it is properly set up for Microsoft365 credentials. TENANT_ID, CLIENT_ID and CLIENT_SECRET are required.", }, - (6006, "Microsoft365GetTokenIdentityError"): { + (6005, "Microsoft365GetTokenIdentityError"): { "message": "Error trying to get token from Microsoft365 Identity", "remediation": "Check the Microsoft365 Identity and ensure it is properly set up.", }, - (6007, "Microsoft365ClientAuthenticationError"): { + (6006, "Microsoft365ClientAuthenticationError"): { "message": "Error in client authentication", "remediation": "Check the client authentication and ensure it is properly set up.", }, - (6008, "Microsoft365NotValidTenantIdError"): { + (6007, "Microsoft365NotValidTenantIdError"): { "message": "The provided tenant ID is not valid", "remediation": "Check the tenant ID and ensure it is a valid ID.", }, - (6009, "Microsoft365NotValidClientIdError"): { + (6008, "Microsoft365NotValidClientIdError"): { "message": "The provided client ID is not valid", "remediation": "Check the client ID and ensure it is a valid ID.", }, - (6010, "Microsoft365NotValidClientSecretError"): { + (6009, "Microsoft365NotValidClientSecretError"): { "message": "The provided client secret is not valid", "remediation": "Check the client secret and ensure it is a valid secret.", }, - (6011, "Microsoft365ConfigCredentialsError"): { + (6010, "Microsoft365ConfigCredentialsError"): { "message": "Error in configuration of Microsoft365 credentials", "remediation": "Check the configuration of Microsoft365 credentials and ensure it is properly set up.", }, - (6012, "Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError"): { + (6011, "Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError"): { "message": "The provided client ID and client secret do not belong to the provided tenant ID", "remediation": "Check the client ID and client secret and ensure they belong to the provided tenant ID.", }, - (6013, "Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError"): { + (6012, "Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError"): { "message": "The provided tenant ID and client secret do not belong to the provided client ID", "remediation": "Check the tenant ID and client secret and ensure they belong to the provided client ID.", }, - (6014, "Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError"): { + (6013, "Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError"): { "message": "The provided tenant ID and client ID do not belong to the provided client secret", "remediation": "Check the tenant ID and client ID and ensure they belong to the provided client secret.", }, - (6015, "Microsoft365InvalidProviderIdError"): { + (6014, "Microsoft365InvalidProviderIdError"): { "message": "The provided provider_id does not match with the available subscriptions", "remediation": "Check the provider_id and ensure it is a valid subscription for the given credentials.", }, - (6016, "Microsoft365NoAuthenticationMethodError"): { - "message": "No Azure authentication method found", - "remediation": "Check that any authentication method is properly set up for Azure.", + (6015, "Microsoft365NoAuthenticationMethodError"): { + "message": "No Microsoft365 authentication method found", + "remediation": "Check that any authentication method is properly set up for Microsoft365.", + }, + (6016, "Microsoft365SetUpSessionError"): { + "message": "Error setting up session", + "remediation": "Check the session setup and ensure it is properly set up.", }, } @@ -114,74 +114,76 @@ def __init__(self, file=None, original_exception=None, message=None): class Microsoft365SetUpRegionConfigError(Microsoft365BaseException): def __init__(self, file=None, original_exception=None, message=None): super().__init__( - 6003, file=file, original_exception=original_exception, message=message + 6002, file=file, original_exception=original_exception, message=message ) -class Microsoft365DefaultAzureCredentialError(Microsoft365CredentialsError): +class Microsoft365HTTPResponseError(Microsoft365BaseException): def __init__(self, file=None, original_exception=None, message=None): super().__init__( 6003, file=file, original_exception=original_exception, message=message ) -class Microsoft365HTTPResponseError(Microsoft365BaseException): +class Microsoft365CredentialsUnavailableError(Microsoft365CredentialsError): def __init__(self, file=None, original_exception=None, message=None): super().__init__( 6004, file=file, original_exception=original_exception, message=message ) -class Microsoft365CredentialsUnavailableError(Microsoft365CredentialsError): +class Microsoft365GetTokenIdentityError(Microsoft365BaseException): def __init__(self, file=None, original_exception=None, message=None): super().__init__( 6005, file=file, original_exception=original_exception, message=message ) -class Microsoft365GetTokenIdentityError(Microsoft365BaseException): +class Microsoft365ClientAuthenticationError(Microsoft365CredentialsError): def __init__(self, file=None, original_exception=None, message=None): super().__init__( 6006, file=file, original_exception=original_exception, message=message ) -class Microsoft365ClientAuthenticationError(Microsoft365CredentialsError): +class Microsoft365NotValidTenantIdError(Microsoft365CredentialsError): def __init__(self, file=None, original_exception=None, message=None): super().__init__( 6007, file=file, original_exception=original_exception, message=message ) -class Microsoft365NotValidTenantIdError(Microsoft365CredentialsError): +class Microsoft365NotValidClientIdError(Microsoft365CredentialsError): def __init__(self, file=None, original_exception=None, message=None): super().__init__( 6008, file=file, original_exception=original_exception, message=message ) -class Microsoft365NotValidClientIdError(Microsoft365CredentialsError): +class Microsoft365NotValidClientSecretError(Microsoft365CredentialsError): def __init__(self, file=None, original_exception=None, message=None): super().__init__( 6009, file=file, original_exception=original_exception, message=message ) -class Microsoft365NotValidClientSecretError(Microsoft365CredentialsError): +class Microsoft365ConfigCredentialsError(Microsoft365CredentialsError): def __init__(self, file=None, original_exception=None, message=None): super().__init__( 6010, file=file, original_exception=original_exception, message=message ) -class Microsoft365ConfigCredentialsError(Microsoft365CredentialsError): +class Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError( + Microsoft365CredentialsError +): def __init__(self, file=None, original_exception=None, message=None): super().__init__( 6011, file=file, original_exception=original_exception, message=message ) -class Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError( +class Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError( Microsoft365CredentialsError ): def __init__(self, file=None, original_exception=None, message=None): @@ -190,7 +192,7 @@ def __init__(self, file=None, original_exception=None, message=None): ) -class Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError( +class Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError( Microsoft365CredentialsError ): def __init__(self, file=None, original_exception=None, message=None): @@ -199,23 +201,21 @@ def __init__(self, file=None, original_exception=None, message=None): ) -class Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError( - Microsoft365CredentialsError -): +class Microsoft365InvalidProviderIdError(Microsoft365BaseException): def __init__(self, file=None, original_exception=None, message=None): super().__init__( 6014, file=file, original_exception=original_exception, message=message ) -class Microsoft365InvalidProviderIdError(Microsoft365BaseException): +class Microsoft365NoAuthenticationMethodError(Microsoft365CredentialsError): def __init__(self, file=None, original_exception=None, message=None): super().__init__( 6015, file=file, original_exception=original_exception, message=message ) -class Microsoft365NoAuthenticationMethodError(Microsoft365CredentialsError): +class Microsoft365SetUpSessionError(Microsoft365CredentialsError): def __init__(self, file=None, original_exception=None, message=None): super().__init__( 6016, file=file, original_exception=original_exception, message=message diff --git a/prowler/providers/microsoft365/lib/arguments/arguments.py b/prowler/providers/microsoft365/lib/arguments/arguments.py index b614cbb9fbd..12a8707264d 100644 --- a/prowler/providers/microsoft365/lib/arguments/arguments.py +++ b/prowler/providers/microsoft365/lib/arguments/arguments.py @@ -8,35 +8,23 @@ def init_parser(self): parents=[self.common_providers_parser], help="Microsoft365 Provider", ) - # Authentication Modes - microsoft365_auth_subparser = microsoft365_parser.add_argument_group( - "Authentication Modes" - ) - microsoft365_auth_modes_group = ( - microsoft365_auth_subparser.add_mutually_exclusive_group() - ) - microsoft365_auth_modes_group.add_argument( - "--app-env-auth", - action="store_true", - help="Use application environment variables authentication to log in against Microsoft 365", - ) # Regions microsoft365_regions_subparser = microsoft365_parser.add_argument_group("Regions") microsoft365_regions_subparser.add_argument( "--microsoft365-region", nargs="?", - default="AzureCloud", + default="Microsoft365Global", type=validate_microsoft365_region, - help="microsoft365 region from `az cloud list --output table`, by default AzureCloud", + help="microsoft365 region from `az cloud list --output table`, by default Microsoft365Global", ) def validate_microsoft365_region(region): """validate_microsoft365_region validates if the region passed as argument is valid""" regions_allowed = [ - "AzureChinaCloud", - "AzureUSGovernment", - "AzureCloud", + "Microsoft365GlobalChina", + "Microsoft365USGovernment", + "Microsoft365Global", ] if region not in regions_allowed: raise ArgumentTypeError( diff --git a/prowler/providers/microsoft365/lib/mutelist/mutelist.py b/prowler/providers/microsoft365/lib/mutelist/mutelist.py index 0e5a82951a3..83d32f4c9ac 100644 --- a/prowler/providers/microsoft365/lib/mutelist/mutelist.py +++ b/prowler/providers/microsoft365/lib/mutelist/mutelist.py @@ -11,7 +11,6 @@ def is_finding_muted( return self.is_muted( finding.tenant_id, finding.check_metadata.CheckID, - finding.tenant_domain, finding.location, finding.resource_name, unroll_dict(unroll_tags(finding.resource_tags)), diff --git a/prowler/providers/microsoft365/lib/regions/regions.py b/prowler/providers/microsoft365/lib/regions/regions.py index 6b88ab5561a..b9e6d1969aa 100644 --- a/prowler/providers/microsoft365/lib/regions/regions.py +++ b/prowler/providers/microsoft365/lib/regions/regions.py @@ -1,26 +1,27 @@ from azure.identity import AzureAuthorityHosts -AZURE_CHINA_CLOUD = "https://management.chinacloudapi.cn" -AZURE_US_GOV_CLOUD = "https://management.usgovcloudapi.net" -AZURE_GENERIC_CLOUD = "https://management.azure.com" +MICROSOFT365_CHINA_CLOUD = "https://microsoftgraph.chinacloudapi.cn" +MICROSOFT365_US_GOV_CLOUD = "https://graph.microsoft.us" +MICROSOFT365_US_DOD_CLOUD = "https://graph.microsoftmil.us" +MICROSOFT365_GENERIC_CLOUD = "https://graph.microsoft.com" def get_regions_config(region): allowed_regions = { - "AzureCloud": { + "Microsoft365Global": { "authority": None, - "base_url": AZURE_GENERIC_CLOUD, - "credential_scopes": [AZURE_GENERIC_CLOUD + "/.default"], + "base_url": MICROSOFT365_GENERIC_CLOUD, + "credential_scopes": [MICROSOFT365_GENERIC_CLOUD + "/.default"], }, - "AzureChinaCloud": { + "Microsoft365China": { "authority": AzureAuthorityHosts.AZURE_CHINA, - "base_url": AZURE_CHINA_CLOUD, - "credential_scopes": [AZURE_CHINA_CLOUD + "/.default"], + "base_url": MICROSOFT365_CHINA_CLOUD, + "credential_scopes": [MICROSOFT365_CHINA_CLOUD + "/.default"], }, - "AzureUSGovernment": { + "Microsoft365USGovernment": { "authority": AzureAuthorityHosts.AZURE_GOVERNMENT, - "base_url": AZURE_US_GOV_CLOUD, - "credential_scopes": [AZURE_US_GOV_CLOUD + "/.default"], + "base_url": MICROSOFT365_US_GOV_CLOUD, + "credential_scopes": [MICROSOFT365_US_GOV_CLOUD + "/.default"], }, } return allowed_regions[region] diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index f6d1cc8f5e2..a26c69f3569 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -6,7 +6,7 @@ from uuid import UUID from azure.core.exceptions import ClientAuthenticationError, HttpResponseError -from azure.identity import ClientSecretCredential, DefaultAzureCredential +from azure.identity import ClientSecretCredential, CredentialUnavailableError from colorama import Fore, Style from msal import ConfidentialClientApplication from msgraph import GraphServiceClient @@ -26,7 +26,6 @@ Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError, Microsoft365ConfigCredentialsError, Microsoft365CredentialsUnavailableError, - Microsoft365DefaultAzureCredentialError, Microsoft365EnvironmentVariableError, Microsoft365GetTokenIdentityError, Microsoft365HTTPResponseError, @@ -35,6 +34,7 @@ Microsoft365NotValidClientSecretError, Microsoft365NotValidTenantIdError, Microsoft365SetUpRegionConfigError, + Microsoft365SetUpSessionError, Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError, Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError, ) @@ -82,7 +82,7 @@ class Microsoft365Provider(Provider): """ _type: str = "microsoft365" - _session: DefaultAzureCredential + _session: ClientSecretCredential _identity: Microsoft365IdentityInfo _audit_config: dict _region_config: Microsoft365RegionConfig @@ -93,7 +93,7 @@ class Microsoft365Provider(Provider): def __init__( self, tenant_id: str = None, - region: str = "AzureCloud", + region: str = "Microsoft365Global", client_id: str = None, client_secret: str = None, config_content: dict = None, @@ -122,7 +122,6 @@ def __init__( Raises: Microsoft365ArgumentTypeValidationError: If there is an error in the argument type validation. Microsoft365SetUpRegionConfigError: If there is an error in setting up the region configuration. - Microsoft365DefaultAzureCredentialError: If there is an error in retrieving the Microsoft365 credentials. Microsoft365ConfigCredentialsError: If there is an error in configuring the Microsoft365 credentials from a dictionary. Microsoft365GetTokenIdentityError: If there is an error in getting the token from the Microsoft365 identity. Microsoft365HTTPResponseError: If there is an HTTP response error. @@ -142,6 +141,7 @@ def __init__( # Set up the Microsoft365 session self._session = self.setup_session( microsoft365_credentials, + self._region_config, ) # Set up the identity @@ -214,21 +214,21 @@ def validate_arguments( client_secret: str, ): """ - Validates the authentication arguments for the Azure provider. + Validates the authentication arguments for the Microsoft365 provider. Args: - tenant_id (str): The Azure Tenant ID. - client_id (str): The Azure Client ID. - client_secret (str): The Azure Client Secret. + tenant_id (str): The Microsoft365 Tenant ID. + client_id (str): The Microsoft365 Client ID. + client_secret (str): The Microsoft365 Client Secret. Raises: - AzureBrowserAuthNoTenantIDError: If browser authentication is enabled but the tenant ID is not found. + """ if not client_id or not client_secret or not tenant_id: raise Microsoft365IdentityInfo( file=os.path.basename(__file__), - message="Tenant Id is required for Azure static credentials. Make sure you are using the correct credentials.", + message="Tenant Id is required for Microsoft365 static credentials. Make sure you are using the correct credentials.", ) @staticmethod @@ -297,6 +297,7 @@ def print_credentials(self): @staticmethod def setup_session( microsoft365_credentials: dict, + region_config: Microsoft365RegionConfig, ): """Returns the Microsoft365 credentials object. @@ -324,17 +325,50 @@ def setup_session( f"{environment_credentials_error.__class__.__name__}[{environment_credentials_error.__traceback__.tb_lineno}] -- {environment_credentials_error}" ) raise environment_credentials_error - if not credentials: - raise Microsoft365CredentialsUnavailableError( - file=os.path.basename(__file__), - message="Failed to retrieve Microsoft365 credentials.", + try: + if microsoft365_credentials: + try: + credentials = ClientSecretCredential( + tenant_id=microsoft365_credentials["tenant_id"], + client_id=microsoft365_credentials["client_id"], + client_secret=microsoft365_credentials["client_secret"], + ) + return credentials + except ClientAuthenticationError as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365ClientAuthenticationError( + file=os.path.basename(__file__), original_exception=error + ) + except CredentialUnavailableError as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365CredentialsUnavailableError( + file=os.path.basename(__file__), original_exception=error + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365ConfigCredentialsError( + file=os.path.basename(__file__), original_exception=error + ) + except Exception as error: + logger.critical("Failed to retrieve Microsoft365 credentials") + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365SetUpSessionError( + file=os.path.basename(__file__), original_exception=error ) return credentials @staticmethod def test_connection( tenant_id=None, - region="AzureCloud", + region="Microsoft365Global", raise_on_exception=True, client_id=None, client_secret=None, @@ -350,7 +384,6 @@ def test_connection( raise_on_exception (bool): Flag indicating whether to raise an exception if the connection fails. client_id (str): The Microsoft365 client ID. client_secret (str): The Microsoft365 client secret. - provider_id (str): The provider ID, in this case it's the Microsoft365 subscription ID. Returns: bool: True if the connection is successful, False otherwise. @@ -359,7 +392,6 @@ def test_connection( Exception: If failed to test the connection to Microsoft365 subscription. Microsoft365ArgumentTypeValidationError: If there is an error in the argument type validation. Microsoft365SetUpRegionConfigError: If there is an error in setting up the region configuration. - Microsoft365DefaultAzureCredentialError: If there is an error in retrieving the Microsoft365 credentials. Microsoft365InteractiveBrowserCredentialError: If there is an error in retrieving the Microsoft365 credentials using browser authentication. Microsoft365HTTPResponseError: If there is an HTTP response error. Microsoft365ConfigCredentialsError: If there is an error in configuring the Microsoft365 credentials from a dictionary. @@ -374,7 +406,7 @@ def test_connection( True """ try: - Microsoft365Provider.setup_region_config(region) + region_config = Microsoft365Provider.setup_region_config(region) # Get the dict from the static credentials microsoft365_credentials = None @@ -388,10 +420,13 @@ def test_connection( ) # Set up the Microsoft365 session - Microsoft365Provider.setup_session( + credentials = Microsoft365Provider.setup_session( microsoft365_credentials, + region_config, ) + GraphServiceClient(credentials=credentials) + logger.info("Microsoft365 provider: Connection to Microsoft365 successful") return Connection(is_connected=True) @@ -419,13 +454,6 @@ def test_connection( if raise_on_exception: raise environment_credentials_error return Connection(error=environment_credentials_error) - except Microsoft365DefaultAzureCredentialError as default_credentials_error: - logger.error( - f"{default_credentials_error.__class__.__name__}[{default_credentials_error.__traceback__.tb_lineno}]: {default_credentials_error}" - ) - if raise_on_exception: - raise default_credentials_error - return Connection(error=default_credentials_error) except Microsoft365ConfigCredentialsError as config_credentials_error: logger.error( f"{config_credentials_error.__class__.__name__}[{config_credentials_error.__traceback__.tb_lineno}]: {config_credentials_error}" @@ -447,13 +475,6 @@ def test_connection( if raise_on_exception: raise credential_unavailable_error return Connection(error=credential_unavailable_error) - except Microsoft365DefaultAzureCredentialError as default_credentials_error: - logger.error( - f"{default_credentials_error.__class__.__name__}[{default_credentials_error.__traceback__.tb_lineno}]: {default_credentials_error}" - ) - if raise_on_exception: - raise default_credentials_error - return Connection(error=default_credentials_error) except ( Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError ) as tenant_id_error: From de1041029419481f4554c2fb72727f6f55b8f263 Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Wed, 4 Dec 2024 07:17:13 +0100 Subject: [PATCH 15/44] feat(microsoft365): Change service to save only one client as multitenant is not allowed for microsoft365 and adjust admincenter service and checks to that change --- .../microsoft365/lib/service/service.py | 22 +-- ...groups_not_public_visibility.metadata.json | 4 +- ...dmincenter_groups_not_public_visibility.py | 28 ++-- .../admincenter/admincenter_service.py | 135 +++++++++--------- ...ns_reduced_license_footprint.metadata.json | 5 +- ..._users_admins_reduced_license_footprint.py | 36 ++--- ...sers_between_two_and_four_global_admins.py | 52 ++++--- 7 files changed, 128 insertions(+), 154 deletions(-) diff --git a/prowler/providers/microsoft365/lib/service/service.py b/prowler/providers/microsoft365/lib/service/service.py index 4863a708a00..3fe72937cd0 100644 --- a/prowler/providers/microsoft365/lib/service/service.py +++ b/prowler/providers/microsoft365/lib/service/service.py @@ -1,6 +1,5 @@ from msgraph import GraphServiceClient -from prowler.lib.logger import logger from prowler.providers.microsoft365.microsoft365_provider import Microsoft365Provider @@ -9,25 +8,10 @@ def __init__( self, provider: Microsoft365Provider, ): - self.clients = self.__set_clients__( - provider.identity, - provider.session, - provider.region_config, - ) + self.client = GraphServiceClient(credentials=provider.session) # self.locations = provider.locations + self.audited_tenant = provider.identity.tenant_id + self.audited_domain = provider.identity.tenant_domain self.audit_config = provider.audit_config self.fixer_config = provider.fixer_config - - def __set_clients__(self, identity, session, region_config): - clients = {} - try: - clients.update( - {identity.tenant_domain: GraphServiceClient(credentials=session)} - ) - except Exception as error: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" - ) - else: - return clients diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json index 32131e6cdc9..d1a975cd539 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json @@ -2,9 +2,7 @@ "Provider": "microsoft365", "CheckID": "admincenter_groups_not_public_visibility", "CheckTitle": "Ensure that only organizationally managed/approved public groups exist", - "CheckType": [ - "Users" - ], + "CheckType": [], "ServiceName": "Admin Center", "SubServiceName": "", "ResourceIdTemplate": "", diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py index c5a5ae95201..d353f4ac8c6 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py @@ -7,21 +7,21 @@ class admincenter_groups_not_public_visibility(Check): def execute(self) -> Check_Report_Microsoft365: findings = [] - for tenant_domain, groups in admincenter_client.groups.items(): - for group_id, group in groups.items(): - report = Check_Report_Microsoft365(self.metadata()) - report.resource_id = group.id - report.resource_name = group.name - report.tenant_id = tenant_domain - report.status = "FAIL" - report.status_extended = f"Group {group.name} has {group.visibility} visibility and should be Private." + for group in admincenter_client.groups.values(): + report = Check_Report_Microsoft365(self.metadata()) + report.resource_id = group.id + report.resource_name = group.name + report.tenant_id = admincenter_client.audited_tenant + report.tenant_domain = admincenter_client.audited_domain + report.status = "FAIL" + report.status_extended = f"Group {group.name} has {group.visibility} visibility and should be Private." - if group.visibility != "Public": - report.status = "PASS" - report.status_extended = ( - f"Group {group.name} has {group.visibility} visibility." - ) + if group.visibility != "Public": + report.status = "PASS" + report.status_extended = ( + f"Group {group.name} has {group.visibility} visibility." + ) - findings.append(report) + findings.append(report) return findings diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_service.py b/prowler/providers/microsoft365/services/admincenter/admincenter_service.py index 2b93b22d5b8..348bc60e460 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_service.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_service.py @@ -32,38 +32,37 @@ async def _get_users(self): logger.info("Microsoft365 - Getting users...") users = {} try: - for tenant, client in self.clients.items(): - users_list = await client.users.get() - users.update({tenant: {}}) - for user in users_list.value: - license_details = await client.users.by_user_id( + users_list = await self.client.users.get() + users.update({}) + for user in users_list.value: + license_details = await self.client.users.by_user_id( + user.id + ).license_details.get() + try: + mailbox_settings = await self.client.users.by_user_id( user.id - ).license_details.get() - try: - mailbox_settings = await client.users.by_user_id( - user.id - ).mailbox_settings.get() - mailbox_settings.user_purpose - except ODataError as error: - if error.error.code == "MailboxNotEnabledForRESTAPI": - pass - else: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" - ) - users[tenant].update( - { - user.id: User( - id=user.id, - name=user.display_name, - license=( - license_details.value[0].sku_part_number - if license_details.value - else None - ), - ) - } - ) + ).mailbox_settings.get() + mailbox_settings.user_purpose + except ODataError as error: + if error.error.code == "MailboxNotEnabledForRESTAPI": + pass + else: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + users.update( + { + user.id: User( + id=user.id, + name=user.display_name, + license=( + license_details.value[0].sku_part_number + if license_details.value + else None + ), + ) + } + ) except Exception as error: logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -75,31 +74,30 @@ async def _get_directory_roles(self): logger.info("Microsoft365 - Getting directory roles...") directory_roles_with_members = {} try: - for tenant, client in self.clients.items(): - directory_roles_with_members.update({tenant: {}}) - directory_roles = await client.directory_roles.get() - for directory_role in directory_roles.value: - directory_role_members = ( - await client.directory_roles.by_directory_role_id( - directory_role.id - ).members.get() - ) - members_with_roles = [] - for member in directory_role_members.value: - user = self.users[tenant].get(member.id, None) - if user: - user.directory_roles.append(directory_role.display_name) - members_with_roles.append(user) - - directory_roles_with_members[tenant].update( - { - directory_role.display_name: DirectoryRole( - id=directory_role.id, - name=directory_role.display_name, - members=members_with_roles, - ) - } - ) + directory_roles_with_members.update({}) + directory_roles = await self.client.directory_roles.get() + for directory_role in directory_roles.value: + directory_role_members = ( + await self.client.directory_roles.by_directory_role_id( + directory_role.id + ).members.get() + ) + members_with_roles = [] + for member in directory_role_members.value: + user = self.users.get(member.id, None) + if user: + user.directory_roles.append(directory_role.display_name) + members_with_roles.append(user) + + directory_roles_with_members.update( + { + directory_role.display_name: DirectoryRole( + id=directory_role.id, + name=directory_role.display_name, + members=members_with_roles, + ) + } + ) except Exception as error: logger.error( @@ -111,19 +109,18 @@ async def _get_groups(self): logger.info("Microsoft365 - Getting groups...") groups = {} try: - for tenant, client in self.clients.items(): - groups_list = await client.groups.get() - groups.update({tenant: {}}) - for group in groups_list.value: - groups[tenant].update( - { - group.id: Group( - id=group.id, - name=group.display_name, - visibility=group.visibility, - ) - } - ) + groups_list = await self.client.groups.get() + groups.update({}) + for group in groups_list.value: + groups.update( + { + group.id: Group( + id=group.id, + name=group.display_name, + visibility=group.visibility, + ) + } + ) except Exception as error: logger.error( diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json index a4b1d8db439..a68c8ae1ced 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json @@ -2,10 +2,7 @@ "Provider": "microsoft365", "CheckID": "admincenter_users_admins_reduced_license_footprint", "CheckTitle": "Ensure administrative accounts use licenses with a reduced application footprint", - "CheckType": [ - "Identity", - "LicenseManagement" - ], + "CheckType": [], "ServiceName": "Admin Center", "SubServiceName": "", "ResourceIdTemplate": "", diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py index 7685abeca85..b146569488a 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py @@ -8,26 +8,26 @@ class admincenter_users_admins_reduced_license_footprint(Check): def execute(self) -> Check_Report_Microsoft365: findings = [] allowed_licenses = ["AAD_PREMIUM", "AAD_PREMIUM_P2"] - for tenant_domain, users in admincenter_client.users.items(): - for user_principal_name, user in users.items(): - admin_roles = [ - role - for role in user.directory_roles - if "Administrator" in role or "Globar Reader" in role - ] + for user_principal_name, user in admincenter_client.users.items(): + admin_roles = [ + role + for role in user.directory_roles + if "Administrator" in role or "Globar Reader" in role + ] - if admin_roles: - report = Check_Report_Microsoft365(self.metadata()) - report.resource_id = user.id - report.resource_name = user.name - report.tenant_id = tenant_domain - report.status = "FAIL" - report.status_extended = f"User {user.name} has administrative roles {admin_roles} and an invalid license {user.license}." + if admin_roles: + report = Check_Report_Microsoft365(self.metadata()) + report.resource_id = user.id + report.resource_name = user.name + report.tenant_id = admincenter_client.audited_tenant + report.tenant_domain = admincenter_client.audited_domain + report.status = "FAIL" + report.status_extended = f"User {user.name} has administrative roles {admin_roles} and an invalid license {user.license}." - if user.license in allowed_licenses: - report.status = "PASS" - report.status_extended = f"User {user.name} has administrative roles {admin_roles} and a valid license: {user.license}." + if user.license in allowed_licenses: + report.status = "PASS" + report.status_extended = f"User {user.name} has administrative roles {admin_roles} and a valid license: {user.license}." - findings.append(report) + findings.append(report) return findings diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py index 97e23b50610..8feda8cf17a 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py @@ -8,34 +8,32 @@ class admincenter_users_between_two_and_four_global_admins(Check): def execute(self) -> Check_Report_Microsoft365: findings = [] - for ( - tenant_domain, - directory_roles, - ) in admincenter_client.directory_roles.items(): - report = Check_Report_Microsoft365(self.metadata()) - report.status = "FAIL" - report.tenant_id = tenant_domain - report.resource_name = "Global Administrator" - - if "Global Administrator" in directory_roles: - report.resource_id = getattr( - directory_roles["Global Administrator"], - "id", - "Global Administrator", + directory_roles = admincenter_client.directory_roles + report = Check_Report_Microsoft365(self.metadata()) + report.status = "FAIL" + report.tenant_id = admincenter_client.audited_tenant + report.tenant_domain = admincenter_client.audited_domain + report.resource_name = "Global Administrator" + + if "Global Administrator" in directory_roles: + report.resource_id = getattr( + directory_roles["Global Administrator"], + "id", + "Global Administrator", + ) + + num_global_admins = len( + getattr(directory_roles["Global Administrator"], "members", []) + ) + + if num_global_admins >= 2 and num_global_admins < 5: + report.status = "PASS" + report.status_extended = ( + f"There are {num_global_admins} global administrators." ) + else: + report.status_extended = f"There are {num_global_admins} global administrators. It should be more than one and less than five." - num_global_admins = len( - getattr(directory_roles["Global Administrator"], "members", []) - ) - - if num_global_admins >= 2 and num_global_admins < 5: - report.status = "PASS" - report.status_extended = ( - f"There are {num_global_admins} global administrators." - ) - else: - report.status_extended = f"There are {num_global_admins} global administrators. It should be more than one and less than five." - - findings.append(report) + findings.append(report) return findings From 6a8c43f42d08fb9039f0c3a7360b6a9cb087cf84 Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Wed, 4 Dec 2024 07:19:13 +0100 Subject: [PATCH 16/44] feat(microsoft365): Adjust tests to service changes --- ...enter_groups_not_public_visibility_test.py | 44 +++------ .../admincenter/admincenter_service_test.py | 56 +++++------- ...s_admins_reduced_license_footprint_test.py | 72 ++++++--------- ...between_two_and_four_global_admins_test.py | 90 ++++++++----------- 4 files changed, 97 insertions(+), 165 deletions(-) diff --git a/tests/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility_test.py b/tests/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility_test.py index 074b3142a22..118aab01be8 100644 --- a/tests/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility_test.py +++ b/tests/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility_test.py @@ -8,8 +8,10 @@ class Test_admincenter_groups_not_public_visibility: - def test_admincenter_no_tenants(self): + def test_admincenter_no_groups(self): admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN with mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -28,28 +30,10 @@ def test_admincenter_no_tenants(self): result = check.execute() assert len(result) == 0 - def test_admincenter_tenant_empty(self): - admincenter_client = mock.MagicMock - - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_microsoft365_provider(), - ), mock.patch( - "prowler.providers.microsoft365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility.admincenter_client", - new=admincenter_client, - ): - from prowler.providers.microsoft365.services.admincenter.admincenter_groups_not_public_visibility.admincenter_groups_not_public_visibility import ( - admincenter_groups_not_public_visibility, - ) - - admincenter_client.groups = {DOMAIN: {}} - - check = admincenter_groups_not_public_visibility() - result = check.execute() - assert len(result) == 0 - def test_admincenter_user_no_admin(self): admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN with mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -65,12 +49,10 @@ def test_admincenter_user_no_admin(self): Group, ) - id_user1 = str(uuid4()) + id_group1 = str(uuid4()) admincenter_client.groups = { - DOMAIN: { - id_user1: Group(id=id_user1, name="Group1", visibility="Private"), - } + id_group1: Group(id=id_group1, name="Group1", visibility="Private"), } check = admincenter_groups_not_public_visibility() @@ -79,10 +61,12 @@ def test_admincenter_user_no_admin(self): assert result[0].status == "PASS" assert result[0].status_extended == "Group Group1 has Private visibility." assert result[0].resource_name == "Group1" - assert result[0].resource_id == id_user1 + assert result[0].resource_id == id_group1 def test_admincenter_user_admin_compliant_license(self): admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN with mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -98,12 +82,10 @@ def test_admincenter_user_admin_compliant_license(self): Group, ) - id_user1 = str(uuid4()) + id_group1 = str(uuid4()) admincenter_client.groups = { - DOMAIN: { - id_user1: Group(id=id_user1, name="Group1", visibility="Private"), - } + id_group1: Group(id=id_group1, name="Group1", visibility="Private"), } check = admincenter_groups_not_public_visibility() @@ -112,4 +94,4 @@ def test_admincenter_user_admin_compliant_license(self): assert result[0].status == "PASS" assert result[0].status_extended == "Group Group1 has Private visibility." assert result[0].resource_name == "Group1" - assert result[0].resource_id == id_user1 + assert result[0].resource_id == id_group1 diff --git a/tests/providers/microsoft365/services/admincenter/admincenter_service_test.py b/tests/providers/microsoft365/services/admincenter/admincenter_service_test.py index ad16429efa4..1aaf11f8f4f 100644 --- a/tests/providers/microsoft365/services/admincenter/admincenter_service_test.py +++ b/tests/providers/microsoft365/services/admincenter/admincenter_service_test.py @@ -1,6 +1,6 @@ from unittest.mock import patch -from prowler.providers.azure.models import AzureIdentityInfo +from prowler.providers.microsoft365.models import Microsoft365IdentityInfo from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( AdminCenter, DirectoryRole, @@ -15,33 +15,27 @@ async def mock_admincenter_get_users(_): return { - DOMAIN: { - "user-1@tenant1.es": User( - id="id-1", - name="User 1", - directory_roles=[], - ), - } + "user-1@tenant1.es": User( + id="id-1", + name="User 1", + directory_roles=[], + ), } async def mock_admincenter_get_directory_roles(_): return { - DOMAIN: { - "GlobalAdministrator": DirectoryRole( - id="id-directory-role", - name="GlobalAdministrator", - members=[], - ) - } + "GlobalAdministrator": DirectoryRole( + id="id-directory-role", + name="GlobalAdministrator", + members=[], + ) } async def mock_admincenter_get_groups(_): return { - DOMAIN: { - "id-1": Group(id="id-1", name="Test", visibility="Public"), - } + "id-1": Group(id="id-1", name="Test", visibility="Public"), } @@ -61,38 +55,30 @@ class Test_AdminCenter_Service: def test_get_client(self): admincenter_client = AdminCenter( set_mocked_microsoft365_provider( - identity=AzureIdentityInfo(tenant_domain=DOMAIN) + identity=Microsoft365IdentityInfo(tenant_domain=DOMAIN) ) ) - assert ( - admincenter_client.clients[DOMAIN].__class__.__name__ - == "GraphServiceClient" - ) + assert admincenter_client.client.__class__.__name__ == "GraphServiceClient" def test_get_users(self): admincenter_client = AdminCenter(set_mocked_microsoft365_provider()) assert len(admincenter_client.users) == 1 - assert admincenter_client.users[DOMAIN]["user-1@tenant1.es"].id == "id-1" - assert admincenter_client.users[DOMAIN]["user-1@tenant1.es"].name == "User 1" + assert admincenter_client.users["user-1@tenant1.es"].id == "id-1" + assert admincenter_client.users["user-1@tenant1.es"].name == "User 1" def test_get_group_settings(self): admincenter_client = AdminCenter(set_mocked_microsoft365_provider()) assert len(admincenter_client.groups) == 1 - assert admincenter_client.groups[DOMAIN]["id-1"].id == "id-1" - assert admincenter_client.groups[DOMAIN]["id-1"].name == "Test" - assert admincenter_client.groups[DOMAIN]["id-1"].visibility == "Public" + assert admincenter_client.groups["id-1"].id == "id-1" + assert admincenter_client.groups["id-1"].name == "Test" + assert admincenter_client.groups["id-1"].visibility == "Public" def test_get_directory_roles(self): admincenter_client = AdminCenter(set_mocked_microsoft365_provider()) assert ( - admincenter_client.directory_roles[DOMAIN]["GlobalAdministrator"].id + admincenter_client.directory_roles["GlobalAdministrator"].id == "id-directory-role" ) assert ( - len( - admincenter_client.directory_roles[DOMAIN][ - "GlobalAdministrator" - ].members - ) - == 0 + len(admincenter_client.directory_roles["GlobalAdministrator"].members) == 0 ) diff --git a/tests/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint_test.py b/tests/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint_test.py index 2098de38f31..91a5453b2a3 100644 --- a/tests/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint_test.py +++ b/tests/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint_test.py @@ -8,8 +8,10 @@ class Test_admincenter_users_admins_reduced_license_footprint: - def test_admincenter_no_tenants(self): + def test_admincenter_no_users(self): admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN with mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -28,28 +30,10 @@ def test_admincenter_no_tenants(self): result = check.execute() assert len(result) == 0 - def test_admincenter_tenant_empty(self): - admincenter_client = mock.MagicMock - - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_microsoft365_provider(), - ), mock.patch( - "prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", - new=admincenter_client, - ): - from prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint import ( - admincenter_users_admins_reduced_license_footprint, - ) - - admincenter_client.users = {DOMAIN: {}} - - check = admincenter_users_admins_reduced_license_footprint() - result = check.execute() - assert len(result) == 0 - def test_admincenter_user_no_admin(self): admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN with mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -68,14 +52,12 @@ def test_admincenter_user_no_admin(self): id_user1 = str(uuid4()) admincenter_client.users = { - DOMAIN: { - id_user1: User( - id=id_user1, - name="User1", - directory_roles=["Exchange User"], - license="O365 BUSINESS", - ), - } + id_user1: User( + id=id_user1, + name="User1", + directory_roles=["Exchange User"], + license="O365 BUSINESS", + ), } check = admincenter_users_admins_reduced_license_footprint() @@ -84,6 +66,8 @@ def test_admincenter_user_no_admin(self): def test_admincenter_user_admin_compliant_license(self): admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN with mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -102,14 +86,12 @@ def test_admincenter_user_admin_compliant_license(self): id_user1 = str(uuid4()) admincenter_client.users = { - DOMAIN: { - id_user1: User( - id=id_user1, - name="User1", - directory_roles=["Global Administrator"], - license="AAD_PREMIUM", - ), - } + id_user1: User( + id=id_user1, + name="User1", + directory_roles=["Global Administrator"], + license="AAD_PREMIUM", + ), } check = admincenter_users_admins_reduced_license_footprint() @@ -125,6 +107,8 @@ def test_admincenter_user_admin_compliant_license(self): def test_admincenter_user_admin_non_compliant_license(self): admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN with mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -143,14 +127,12 @@ def test_admincenter_user_admin_non_compliant_license(self): id_user1 = str(uuid4()) admincenter_client.users = { - DOMAIN: { - id_user1: User( - id=id_user1, - name="User1", - directory_roles=["Global Administrator"], - license="O365 BUSINESS", - ), - } + id_user1: User( + id=id_user1, + name="User1", + directory_roles=["Global Administrator"], + license="O365 BUSINESS", + ), } check = admincenter_users_admins_reduced_license_footprint() diff --git a/tests/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins_test.py b/tests/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins_test.py index da9bf37799f..6416682e93b 100644 --- a/tests/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins_test.py +++ b/tests/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins_test.py @@ -8,8 +8,10 @@ class Test_admincenter_users_between_two_and_four_global_admins: - def test_admincenter_no_tenants(self): + def test_admincenter_no_directory_roles(self): admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN with mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -28,28 +30,10 @@ def test_admincenter_no_tenants(self): result = check.execute() assert len(result) == 0 - def test_admincenter_tenant_empty(self): - admincenter_client = mock.MagicMock - - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_microsoft365_provider(), - ), mock.patch( - "prowler.providers.microsoft365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins.admincenter_client", - new=admincenter_client, - ): - from prowler.providers.microsoft365.services.admincenter.admincenter_users_between_two_and_four_global_admins.admincenter_users_between_two_and_four_global_admins import ( - admincenter_users_between_two_and_four_global_admins, - ) - - admincenter_client.directory_roles = {DOMAIN: {}} - - check = admincenter_users_between_two_and_four_global_admins() - result = check.execute() - assert len(result) == 0 - def test_admincenter_less_than_five_global_admins(self): admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN with mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -71,16 +55,14 @@ def test_admincenter_less_than_five_global_admins(self): id_user2 = str(uuid4()) admincenter_client.directory_roles = { - DOMAIN: { - "Global Administrator": DirectoryRole( - id=id, - name="Global Administrator", - members=[ - User(id=id_user1, name="User1"), - User(id=id_user2, name="User2"), - ], - ) - } + "Global Administrator": DirectoryRole( + id=id, + name="Global Administrator", + members=[ + User(id=id_user1, name="User1"), + User(id=id_user2, name="User2"), + ], + ) } check = admincenter_users_between_two_and_four_global_admins() @@ -93,6 +75,8 @@ def test_admincenter_less_than_five_global_admins(self): def test_admincenter_more_than_five_global_admins(self): admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN with mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -118,20 +102,18 @@ def test_admincenter_more_than_five_global_admins(self): id_user6 = str(uuid4()) admincenter_client.directory_roles = { - DOMAIN: { - "Global Administrator": DirectoryRole( - id=id, - name="Global Administrator", - members=[ - User(id=id_user1, name="User1"), - User(id=id_user2, name="User2"), - User(id=id_user3, name="User3"), - User(id=id_user4, name="User4"), - User(id=id_user5, name="User5"), - User(id=id_user6, name="User6"), - ], - ) - } + "Global Administrator": DirectoryRole( + id=id, + name="Global Administrator", + members=[ + User(id=id_user1, name="User1"), + User(id=id_user2, name="User2"), + User(id=id_user3, name="User3"), + User(id=id_user4, name="User4"), + User(id=id_user5, name="User5"), + User(id=id_user6, name="User6"), + ], + ) } check = admincenter_users_between_two_and_four_global_admins() @@ -147,6 +129,8 @@ def test_admincenter_more_than_five_global_admins(self): def test_admincenter_one_global_admin(self): admincenter_client = mock.MagicMock + admincenter_client.audited_tenant = "audited_tenant" + admincenter_client.audited_domain = DOMAIN with mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -167,15 +151,13 @@ def test_admincenter_one_global_admin(self): id_user1 = str(uuid4()) admincenter_client.directory_roles = { - DOMAIN: { - "Global Administrator": DirectoryRole( - id=id, - name="Global Administrator", - members=[ - User(id=id_user1, name="User1"), - ], - ) - } + "Global Administrator": DirectoryRole( + id=id, + name="Global Administrator", + members=[ + User(id=id_user1, name="User1"), + ], + ) } check = admincenter_users_between_two_and_four_global_admins() From 5a88a60c6de41773db83f2021d1c7e08cf1dc9e2 Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Wed, 4 Dec 2024 14:49:39 +0100 Subject: [PATCH 17/44] feat(microsoft365): Fill compliance with the done checks and put manual to those that are either manual in the CIS or cant be done using msgraph SDK --- .../microsoft365/cis_4.0_microsoft365.json | 124 ++++++++++++++++-- 1 file changed, 114 insertions(+), 10 deletions(-) diff --git a/prowler/compliance/microsoft365/cis_4.0_microsoft365.json b/prowler/compliance/microsoft365/cis_4.0_microsoft365.json index 10cfbd7c8fd..ca26b4a467a 100644 --- a/prowler/compliance/microsoft365/cis_4.0_microsoft365.json +++ b/prowler/compliance/microsoft365/cis_4.0_microsoft365.json @@ -6,23 +6,127 @@ "Requirements": [ { "Id": "1.1.1", - "Description": "Ensure that 'Administrative accounts' are 'cloud-only'", + "Description": "Ensure Administrative accounts are cloud-only", + "Checks": [], + "Attributes": [ + { + "Section": "1.Microsoft 365 admin center", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Administrative accounts are special privileged accounts that could have varying levels of access to data, users, and settings. Regular user accounts should never be utilized for administrative tasks and care should be taken, in the case of a hybrid environment, to keep Administrative accounts separated from on-prem accounts. Administrative accounts should not have applications assigned so that they have no access to potentially vulnerable services (e.g., email, Teams, SharePoint, etc.) and only access to perform tasks as needed for administrative purposes. Ensure administrative accounts are not On-premises sync enabled.", + "RationaleStatement": "In a hybrid environment, having separate accounts will help ensure that in the event of a breach in the cloud, that the breach does not affect the on-prem environment and vice versa.", + "ImpactStatement": "Administrative users will have to switch accounts and utilize login/logout functionality when performing administrative tasks, as well as not benefiting from SSO. This will require a migration process from the 'daily driver' account to a dedicated admin account. When migrating permissions to the admin account, both M365 and Azure RBAC roles should be migrated as well. Once the new admin accounts are created, both of these permission sets should be moved from the daily driver account to the new admin account. Failure to migrate Azure RBAC roles can cause an admin to not be able to see their subscriptions/resources while using their admin accounts.", + "RemediationProcedure": "Review all administrative accounts and ensure they are configured as cloud-only. Remove any on-premises synchronization for these accounts. Assign necessary roles and permissions exclusively to the dedicated cloud administrative accounts.", + "AuditProcedure": "Log in to the Microsoft 365 Admin Center and review the list of administrative accounts. Verify that none of them are on-premises sync enabled.", + "AdditionalInformation": "This recommendation is particularly relevant for hybrid environments and is aimed at enhancing the security of administrative accounts by isolating them from on-prem infrastructure.", + "DefaultValue": "By default, administrative accounts may be either cloud-only or hybrid. This setting needs to be verified and adjusted according to the recommendation.", + "References": "CIS Microsoft 365 Foundations Benchmark v4.0, Section 1.1.1" + } + ] + }, + { + "Id": "1.1.2", + "Description": "Ensure two emergency access accounts have been defined", + "Checks": [], + "Attributes": [ + { + "Section": "1.Microsoft 365 admin center", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Emergency access or 'break glass' accounts are limited for emergency scenarios where normal administrative accounts are unavailable. They are not assigned to a specific user and will have a combination of physical and technical controls to prevent them from being accessed outside a true emergency. These emergencies could include technical failures of a cellular provider or Microsoft-related service such as MFA, or the last remaining Global Administrator account becoming inaccessible. Ensure two Emergency Access accounts have been defined.", + "RationaleStatement": "In various situations, an organization may require the use of a break glass account to gain emergency access. Losing access to administrative functions could result in a significant loss of support capability, reduced visibility into the security posture, and potential financial losses.", + "ImpactStatement": "Improper implementation of emergency access accounts could weaken the organization's security posture. To mitigate this, at least one account should be excluded from all conditional access rules, and strong authentication mechanisms (e.g., long, high-entropy passwords or FIDO2 security keys) must be used to secure the accounts.", + "RemediationProcedure": "Create two emergency access accounts and configure them according to Microsoft's recommendations. Ensure that these accounts are not assigned to specific users and are excluded from all conditional access rules. Secure the accounts with strong passwords or passwordless authentication methods, such as FIDO2 security keys. Regularly review and test these accounts to confirm their functionality.", + "AuditProcedure": "Log in to the Microsoft 365 Admin Center and verify the existence of at least two emergency access accounts. Check their configurations to ensure they comply with Microsoft's recommendations, including exclusion from conditional access rules and proper security settings.", + "AdditionalInformation": "Emergency access accounts are critical for maintaining administrative control during unexpected events. Regular audits and strict access controls are recommended to prevent misuse.", + "DefaultValue": "By default, emergency access accounts are not configured. Organizations must create and secure these accounts proactively.", + "References": "CIS Microsoft 365 Foundations Benchmark v4.0, Section 1.1.2; Microsoft documentation on emergency access accounts." + } + ] + }, + { + "Id": "1.1.3", + "Description": "Ensure that between two and four global admins are designated", + "Checks": [ + "admincenter_users_between_two_and_four_global_admins" + ], + "Attributes": [ + { + "Section": "1.Microsoft 365 admin center", + "Profile": "Level 1", + "AssessmentStatus": "Automated", + "Description": "More than one global administrator should be designated so a single admin can be monitored and to provide redundancy should a single admin leave an organization. Additionally, there should be no more than four global admins set for any tenant. Ideally, global administrators will have no licenses assigned to them.", + "RationaleStatement": "If there is only one global tenant administrator, he or she can perform malicious activity without the possibility of being discovered by another admin. If there are numerous global tenant administrators, the more likely it is that one of their accounts will be successfully breached by an external attacker.", + "ImpactStatement": "If there is only one global administrator in a tenant, an additional global administrator will need to be identified and configured. If there are more than four global administrators, a review of role requirements for current global administrators will be required to identify which of the users require global administrator access.", + "RemediationProcedure": "Review the list of global administrators in the tenant and ensure there are at least two but no more than four accounts assigned this role. Remove excess global administrator accounts and create additional ones if necessary. Avoid assigning licenses to these accounts.", + "AuditProcedure": "Log in to the Microsoft 365 Admin Center and review the list of global administrators. Verify that there are at least two but no more than four global administrators configured.", + "AdditionalInformation": "Global administrators play a critical role in tenant management. Ensuring a proper number of global administrators improves redundancy and security.", + "DefaultValue": "By default, there may be a single global administrator configured for the tenant. Organizations need to manually adjust the count as per best practices.", + "References": "CIS Microsoft 365 Foundations Benchmark v4.0, Section 1.1.3" + } + ] + }, + { + "Id": "1.1.4", + "Description": "Ensure administrative accounts use licenses with a reduced application footprint", "Checks": [ - "entra_policy_ensure_default_user_cannot_create_tenants" + "admincenter_users_admins_reduced_license_footprint" ], "Attributes": [ { "Section": "1.Microsoft 365 admin center", "Profile": "Level 1", "AssessmentStatus": "Automated", - "Description": "", - "RationaleStatement": "", - "ImpactStatement": "", - "RemediationProcedure": "", - "AuditProcedure": "", - "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "Description": "Administrative accounts are special privileged accounts with varying levels of access to data, users, and settings. It is recommended that privileged accounts either not be licensed or use Microsoft Entra ID P1 or Microsoft Entra ID P2 licenses to minimize application exposure.", + "RationaleStatement": "Ensuring administrative accounts do not use licenses with applications assigned to them reduces the attack surface of high-privileged identities. This minimizes the likelihood of these accounts being targeted by social engineering attacks or exposed to malicious content via licensed applications. Administrative activities should be restricted to dedicated accounts without access to collaborative tools like mailboxes.", + "ImpactStatement": "Administrative users will need to switch accounts to perform privileged actions, requiring login/logout functionality and potentially losing the convenience of SSO. Alerts sent to Global Administrators or TenantAdmins by default might not be received if these accounts lack application-based licenses. Proper alert routing must be configured to avoid missed notifications.", + "RemediationProcedure": "Review the licenses assigned to administrative accounts. Remove licenses granting access to collaborative applications and assign Microsoft Entra ID P1 or P2 licenses if participation in Microsoft 365 security services is required. Configure alerts to be sent to valid email addresses for monitoring.", + "AuditProcedure": "Log in to the Microsoft 365 Admin Center and review the licenses assigned to administrative accounts. Confirm that administrative accounts either have no licenses or are limited to Microsoft Entra ID P1 or P2 licenses without collaborative applications enabled.", + "AdditionalInformation": "Reducing the application footprint of administrative accounts improves security by minimizing potential attack vectors. Special care should be taken to configure alert routing properly to ensure critical notifications are not missed.", + "DefaultValue": "By default, administrative accounts may have licenses assigned based on organizational setup. Manual review and adjustment are necessary to comply with this recommendation.", + "References": "CIS Microsoft 365 Foundations Benchmark v4.0, Section 1.1.4; Microsoft documentation on Entra ID licenses and privileged account security." + } + ] + }, + { + "Id": "1.2.1", + "Description": "Ensure that only organizationally managed/approved public groups exist", + "Checks": [ + "admincenter_groups_not_public_visibility" + ], + "Attributes": [ + { + "Section": "1.Microsoft 365 admin center", + "Profile": "Level 2", + "AssessmentStatus": "Automated", + "Description": "Microsoft 365 Groups enable shared resource access across Microsoft 365 services. The default privacy setting for groups is 'Public,' which allows users within the organization to access the group's resources. Ensure that only organizationally managed and approved public groups exist to prevent unauthorized access to sensitive information.", + "RationaleStatement": "Public groups can be accessed by any user within the organization via several methods, such as self-adding through the Azure portal, sending an access request, or directly using the SharePoint URL. Without control over group privacy, sensitive organizational data might be exposed to unintended users.", + "ImpactStatement": "Implementing this recommendation may result in an increased volume of access requests for group owners, particularly for groups previously intended to be public.", + "RemediationProcedure": "Audit all Microsoft 365 public groups and ensure they are organizationally managed and approved. Convert unnecessary public groups to private groups and enforce strict policies for creating and approving public groups. Group owners should be instructed to monitor and review access requests.", + "AuditProcedure": "Log in to the Microsoft 365 Admin Center and review the list of public groups. Verify that all public groups have been approved and are necessary for organizational purposes.", + "AdditionalInformation": "Public groups expose data to all users within the organization, increasing the risk of accidental or unauthorized access. Periodic reviews of group privacy settings are recommended.", + "DefaultValue": "By default, groups created in Microsoft 365 are set to 'Public' privacy.", + "References": "CIS Microsoft 365 Foundations Benchmark v4.0, Section 1.2.1; Microsoft documentation on managing group privacy." + } + ] + }, + { + "Id": "1.2.2", + "Description": "Ensure sign-in to shared mailboxes is blocked", + "Checks": [], + "Attributes": [ + { + "Section": "1.Microsoft 365 admin center", + "Profile": "Level 1", + "AssessmentStatus": "Manuel", + "Description": "Shared mailboxes are used when multiple people need access to the same mailbox for functions such as support or reception. These mailboxes are created with a corresponding user account that includes a system-generated password. To enhance security, sign-in should be blocked for these shared mailbox accounts, ensuring access is granted only through delegation.", + "RationaleStatement": "Blocking sign-in for shared mailbox accounts prevents unauthorized access or direct sign-in, ensuring that all interactions with the shared mailbox are through authorized delegation. This reduces the risk of attackers exploiting shared mailboxes for malicious purposes such as sending emails with spoofed identities.", + "ImpactStatement": "Blocking sign-in to shared mailboxes requires users to access these mailboxes only through delegation. Administrators will need to monitor and ensure proper access permissions are assigned.", + "RemediationProcedure": "Log in to the Microsoft 365 Admin Center and locate the shared mailboxes. For each shared mailbox, verify that sign-in is blocked by reviewing the associated user account settings. If sign-in is not blocked, adjust the account settings to enforce this configuration.", + "AuditProcedure": "Review all shared mailboxes in the Microsoft 365 Admin Center. Confirm that the user accounts associated with these mailboxes have sign-in blocked.", + "AdditionalInformation": "Shared mailboxes are often a target for exploitation due to their broad access and functional role. Blocking sign-in significantly reduces the attack surface.", + "DefaultValue": "By default, shared mailboxes may have sign-in enabled unless explicitly configured otherwise.", + "References": "CIS Microsoft 365 Foundations Benchmark v4.0, Section 1.2.2; Microsoft documentation on managing shared mailboxes." } ] } From ff81f4db4e36f9a1e3180dc46dafee0a90daef86 Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Wed, 18 Dec 2024 16:48:13 +0100 Subject: [PATCH 18/44] fix(admincenter): Fix metadata service in the checks so scan by service works --- .../admincenter_groups_not_public_visibility.metadata.json | 2 +- ...ncenter_users_admins_reduced_license_footprint.metadata.json | 2 +- ...enter_users_between_two_and_four_global_admins.metadata.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json index d1a975cd539..1bfc5cf677a 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.metadata.json @@ -3,7 +3,7 @@ "CheckID": "admincenter_groups_not_public_visibility", "CheckTitle": "Ensure that only organizationally managed/approved public groups exist", "CheckType": [], - "ServiceName": "Admin Center", + "ServiceName": "admincenter", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json index a68c8ae1ced..c141732dd47 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.metadata.json @@ -3,7 +3,7 @@ "CheckID": "admincenter_users_admins_reduced_license_footprint", "CheckTitle": "Ensure administrative accounts use licenses with a reduced application footprint", "CheckType": [], - "ServiceName": "Admin Center", + "ServiceName": "admincenter", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json index 57e5ee139b0..f5cfbd87e73 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json @@ -3,7 +3,7 @@ "CheckID": "admincenter_users_between_two_and_four_global_admins", "CheckTitle": "Ensure that between two and four global admins are designated", "CheckType": [], - "ServiceName": "Admin Center", + "ServiceName": "admincenter", "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", From b366afed526310057bdafe831281a7522d50aac6 Mon Sep 17 00:00:00 2001 From: MarioRgzLpz Date: Thu, 19 Dec 2024 06:10:36 +0100 Subject: [PATCH 19/44] fix: Delete categories in the check metadata --- ...er_users_between_two_and_four_global_admins.metadata.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json index f5cfbd87e73..fafaf16cd14 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.metadata.json @@ -23,10 +23,7 @@ "Url": "https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/manage-roles-portal" } }, - "Categories": [ - "identity-management", - "privilege-management" - ], + "Categories": [], "DependsOn": [], "RelatedTo": [], "Notes": "" From 04964d25ad45c0523236abd7837fb49d9d161c90 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero Date: Thu, 16 Jan 2025 11:12:02 +0100 Subject: [PATCH 20/44] fix(m365): fix provider test --- tests/providers/azure/azure_provider_test.py | 2 +- .../microsoft365/microsoft365_fixtures.py | 1 + .../microsoft365_provider_test.py | 41 +++++++++++++++---- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/tests/providers/azure/azure_provider_test.py b/tests/providers/azure/azure_provider_test.py index 853903fcc2e..e754964e393 100644 --- a/tests/providers/azure/azure_provider_test.py +++ b/tests/providers/azure/azure_provider_test.py @@ -438,7 +438,7 @@ def test_test_connection_with_exception(self): raise_on_exception=True, ) - assert exception.type == Exception + assert exception.type is Exception assert exception.value.args[0] == "Simulated Exception" @pytest.mark.parametrize( diff --git a/tests/providers/microsoft365/microsoft365_fixtures.py b/tests/providers/microsoft365/microsoft365_fixtures.py index 5d6b30caf9d..a27c43e6fac 100644 --- a/tests/providers/microsoft365/microsoft365_fixtures.py +++ b/tests/providers/microsoft365/microsoft365_fixtures.py @@ -12,6 +12,7 @@ IDENTITY_TYPE = "Application" TENANT_ID = "00000000-0000-0000-0000-000000000000" DOMAIN = "user.onmicrosoft.com" +LOCATION = "global" # Mocked Azure Audit Info diff --git a/tests/providers/microsoft365/microsoft365_provider_test.py b/tests/providers/microsoft365/microsoft365_provider_test.py index 5c951f16fdb..d15cd4198e7 100644 --- a/tests/providers/microsoft365/microsoft365_provider_test.py +++ b/tests/providers/microsoft365/microsoft365_provider_test.py @@ -2,6 +2,7 @@ from uuid import uuid4 import pytest +from azure.identity import ClientSecretCredential from mock import MagicMock from prowler.config.config import ( @@ -15,6 +16,13 @@ Microsoft365IdentityInfo, Microsoft365RegionConfig, ) +from tests.providers.microsoft365.microsoft365_fixtures import ( + DOMAIN, + IDENTITY_ID, + IDENTITY_TYPE, + LOCATION, + TENANT_ID, +) class TestMicrosoft365Provider: @@ -28,9 +36,25 @@ def test_microsoft365_provider(self): ) azure_region = "Microsoft365Global" - with patch( - "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_identity", - return_value=Microsoft365IdentityInfo(), + with ( + patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session", + return_value=ClientSecretCredential( + client_id=IDENTITY_ID, + tenant_id=TENANT_ID, + client_secret="client_secret", + ), + ), + patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_identity", + return_value=Microsoft365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type=IDENTITY_TYPE, + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + location=LOCATION, + ), + ), ): microsoft365_provider = Microsoft365Provider( tenant_id, @@ -48,10 +72,11 @@ def test_microsoft365_provider(self): credential_scopes=["https://graph.microsoft.com/.default"], ) assert microsoft365_provider.identity == Microsoft365IdentityInfo( - identity_id="", - identity_type="", - tenant_id="", - tenant_domain="Unknown tenant domain (missing AAD permissions)", + identity_id=IDENTITY_ID, + identity_type=IDENTITY_TYPE, + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + location=LOCATION, ) def test_test_connection_tenant_id_client_id_client_secret(self): @@ -92,5 +117,5 @@ def test_test_connection_with_exception(self): raise_on_exception=True, ) - assert exception.type == Exception + assert exception.type is Exception assert exception.value.args[0] == "Simulated Exception" From fc64e5085366bf3378c81bb9debd2de44caefe46 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero Date: Thu, 16 Jan 2025 14:02:50 +0100 Subject: [PATCH 21/44] chore(m365): fix regions test --- .../microsoft365/lib/regions/regions_test.py | 34 +++++++++---------- .../admincenter/lib/user_privileges_test.py | 25 -------------- 2 files changed, 17 insertions(+), 42 deletions(-) delete mode 100644 tests/providers/microsoft365/services/admincenter/lib/user_privileges_test.py diff --git a/tests/providers/microsoft365/lib/regions/regions_test.py b/tests/providers/microsoft365/lib/regions/regions_test.py index 2f8fdd60533..49ec5a480dc 100644 --- a/tests/providers/microsoft365/lib/regions/regions_test.py +++ b/tests/providers/microsoft365/lib/regions/regions_test.py @@ -1,35 +1,35 @@ from azure.identity import AzureAuthorityHosts -from prowler.providers.azure.lib.regions.regions import ( - AZURE_CHINA_CLOUD, - AZURE_GENERIC_CLOUD, - AZURE_US_GOV_CLOUD, +from prowler.providers.microsoft365.lib.regions.regions import ( + MICROSOFT365_CHINA_CLOUD, + MICROSOFT365_GENERIC_CLOUD, + MICROSOFT365_US_GOV_CLOUD, get_regions_config, ) -class Test_azure_regions: +class Test_microsoft365_regions: def test_get_regions_config(self): allowed_regions = [ - "AzureCloud", - "AzureChinaCloud", - "AzureUSGovernment", + "Microsoft365Global", + "Microsoft365China", + "Microsoft365USGovernment", ] expected_output = { - "AzureCloud": { + "Microsoft365Global": { "authority": None, - "base_url": AZURE_GENERIC_CLOUD, - "credential_scopes": [AZURE_GENERIC_CLOUD + "/.default"], + "base_url": MICROSOFT365_GENERIC_CLOUD, + "credential_scopes": [MICROSOFT365_GENERIC_CLOUD + "/.default"], }, - "AzureChinaCloud": { + "Microsoft365China": { "authority": AzureAuthorityHosts.AZURE_CHINA, - "base_url": AZURE_CHINA_CLOUD, - "credential_scopes": [AZURE_CHINA_CLOUD + "/.default"], + "base_url": MICROSOFT365_CHINA_CLOUD, + "credential_scopes": [MICROSOFT365_CHINA_CLOUD + "/.default"], }, - "AzureUSGovernment": { + "Microsoft365USGovernment": { "authority": AzureAuthorityHosts.AZURE_GOVERNMENT, - "base_url": AZURE_US_GOV_CLOUD, - "credential_scopes": [AZURE_US_GOV_CLOUD + "/.default"], + "base_url": MICROSOFT365_US_GOV_CLOUD, + "credential_scopes": [MICROSOFT365_US_GOV_CLOUD + "/.default"], }, } diff --git a/tests/providers/microsoft365/services/admincenter/lib/user_privileges_test.py b/tests/providers/microsoft365/services/admincenter/lib/user_privileges_test.py deleted file mode 100644 index bf2a75e44c4..00000000000 --- a/tests/providers/microsoft365/services/admincenter/lib/user_privileges_test.py +++ /dev/null @@ -1,25 +0,0 @@ -from unittest import mock -from uuid import uuid4 - -from prowler.providers.azure.services.entra.entra_service import User -from prowler.providers.azure.services.entra.lib.user_privileges import ( - is_privileged_user, -) - - -class Test_user_privileges_test: - def test_user_in_privileged_roles(self): - user_id = str(uuid4()) - privileged_roles = {"admin": mock.MagicMock()} - privileged_roles["admin"].members = [User(id=user_id, name="user1")] - - user = User(id=user_id, name="user1") - assert is_privileged_user(user, privileged_roles) - - def test_user_not_in_privileged_roles(self): - user_id = str(uuid4()) - privileged_roles = {"admin": mock.MagicMock()} - privileged_roles["admin"].members = [User(id=str(uuid4()), name="user2")] - - user = User(id=user_id, name="user1") - assert not is_privileged_user(user, privileged_roles) From 72b8da1b3781385fbab526c9963c2ec1a4d006dd Mon Sep 17 00:00:00 2001 From: Daniel Barranquero Date: Thu, 16 Jan 2025 16:37:29 +0100 Subject: [PATCH 22/44] fix(m365): modify regions file name --- .../lib/regions/{regions.py => microsoft365_regions.py} | 0 prowler/providers/microsoft365/microsoft365_provider.py | 4 +++- .../regions/{regions_test.py => microsoft365_regions_test.py} | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) rename prowler/providers/microsoft365/lib/regions/{regions.py => microsoft365_regions.py} (100%) rename tests/providers/microsoft365/lib/regions/{regions_test.py => microsoft365_regions_test.py} (94%) diff --git a/prowler/providers/microsoft365/lib/regions/regions.py b/prowler/providers/microsoft365/lib/regions/microsoft365_regions.py similarity index 100% rename from prowler/providers/microsoft365/lib/regions/regions.py rename to prowler/providers/microsoft365/lib/regions/microsoft365_regions.py diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index a26c69f3569..c285c2d6ac0 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -42,7 +42,9 @@ validate_microsoft365_region, ) from prowler.providers.microsoft365.lib.mutelist.mutelist import Microsoft365Mutelist -from prowler.providers.microsoft365.lib.regions.regions import get_regions_config +from prowler.providers.microsoft365.lib.regions.microsoft365_regions import ( + get_regions_config, +) from prowler.providers.microsoft365.models import ( Microsoft365IdentityInfo, Microsoft365RegionConfig, diff --git a/tests/providers/microsoft365/lib/regions/regions_test.py b/tests/providers/microsoft365/lib/regions/microsoft365_regions_test.py similarity index 94% rename from tests/providers/microsoft365/lib/regions/regions_test.py rename to tests/providers/microsoft365/lib/regions/microsoft365_regions_test.py index 49ec5a480dc..3e358511b8b 100644 --- a/tests/providers/microsoft365/lib/regions/regions_test.py +++ b/tests/providers/microsoft365/lib/regions/microsoft365_regions_test.py @@ -1,6 +1,6 @@ from azure.identity import AzureAuthorityHosts -from prowler.providers.microsoft365.lib.regions.regions import ( +from prowler.providers.microsoft365.lib.regions.microsoft365_regions import ( MICROSOFT365_CHINA_CLOUD, MICROSOFT365_GENERIC_CLOUD, MICROSOFT365_US_GOV_CLOUD, From e5ca040fec3989640ddebfdd5d1a325eac0632c0 Mon Sep 17 00:00:00 2001 From: HugoPBrito Date: Fri, 17 Jan 2025 11:04:28 +0100 Subject: [PATCH 23/44] feat: mutelist tested and mutelist example added --- .../config/microsoft365_mutelist_example.yaml | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 prowler/config/microsoft365_mutelist_example.yaml diff --git a/prowler/config/microsoft365_mutelist_example.yaml b/prowler/config/microsoft365_mutelist_example.yaml new file mode 100644 index 00000000000..c33b442c04f --- /dev/null +++ b/prowler/config/microsoft365_mutelist_example.yaml @@ -0,0 +1,44 @@ +### Account, Check and/or Region can be * to apply for all the cases. +### Account == Microsoft365 Tenant and Region == Microsoft365 Location +### Resources and tags are lists that can have either Regex or Keywords. +### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together. +### Use an alternation Regex to match one of multiple tags with "ORed" logic. +### For each check you can except Accounts, Regions, Resources and/or Tags. +########################### MUTELIST EXAMPLE ########################### +Mutelist: + Accounts: + "*": + Checks: + "admincenter_groups_not_public_visibility": + Regions: + - "westeurope" + Resources: + - "sqlserver1" # Will ignore sqlserver1 in check sqlserver_tde_encryption_enabled located in westeurope + Description: "Findings related with the check sqlserver_tde_encryption_enabled is muted for westeurope region and sqlserver1 resource" + "defender_*": + Regions: + - "*" + Resources: + - "*" # Will ignore every Defender check in every location + "*": + Regions: + - "*" + Resources: + - "test" + Tags: + - "test=test" # Will ignore every resource containing the string "test" and the tags 'test=test' and + - "project=test|project=stage" # either of ('project=test' OR project=stage) in Azure subscription 1 and every location + + "*": + Checks: + "admincenter_*": + Regions: + - "*" + Resources: + - "*" + Exceptions: + Accounts: + - "Tenant1" + Regions: + - "eastus" + - "eastus2" # Will ignore every resource in admincenter checks except the ones in Tenant1 located in eastus or eastus2 From ff1083224e5062368d67d6c813b8e716f16804e8 Mon Sep 17 00:00:00 2001 From: HugoPBrito Date: Mon, 20 Jan 2025 17:15:08 +0100 Subject: [PATCH 24/44] chore: enhanced exepction management --- .../microsoft365/services/admincenter/admincenter_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_service.py b/prowler/providers/microsoft365/services/admincenter/admincenter_service.py index 348bc60e460..2fc14d1ae6e 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_service.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_service.py @@ -45,7 +45,9 @@ async def _get_users(self): mailbox_settings.user_purpose except ODataError as error: if error.error.code == "MailboxNotEnabledForRESTAPI": - pass + logger.warning( + f"MailboxNotEnabledForRESTAPI for user {user.id}" + ) else: logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" From 90e665edee710ef79ff562763497f376bda34d32 Mon Sep 17 00:00:00 2001 From: MrCloudSec Date: Tue, 21 Jan 2025 12:00:54 -0500 Subject: [PATCH 25/44] chore: add m365 to help --- prowler/lib/cli/parser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/prowler/lib/cli/parser.py b/prowler/lib/cli/parser.py index 868de4d51c0..fc5b2bc3ba7 100644 --- a/prowler/lib/cli/parser.py +++ b/prowler/lib/cli/parser.py @@ -34,6 +34,7 @@ def __init__(self): azure Azure Provider gcp GCP Provider kubernetes Kubernetes Provider + microsoft365 Microsoft 365 Provider Available components: dashboard Local dashboard From ec31c0bcb20de1e0257240c963a4082ab1e95bb3 Mon Sep 17 00:00:00 2001 From: MrCloudSec Date: Tue, 21 Jan 2025 16:44:56 -0500 Subject: [PATCH 26/44] chore: revision and add compliance --- prowler/__main__.py | 32 +++++- prowler/compliance/microsoft365/__init__.py | 0 prowler/config/config.py | 1 + prowler/lib/cli/parser.py | 4 +- .../compliance/cis/cis_microsoft365.py | 99 +++++++++++++++++++ prowler/lib/outputs/compliance/cis/models.py | 31 ++++++ prowler/providers/common/provider.py | 1 + .../microsoft365/lib/arguments/arguments.py | 2 +- .../microsoft365/microsoft365_provider.py | 6 -- ..._users_admins_reduced_license_footprint.py | 2 +- tests/lib/cli/parser_test.py | 6 +- 11 files changed, 169 insertions(+), 15 deletions(-) create mode 100644 prowler/compliance/microsoft365/__init__.py create mode 100644 prowler/lib/outputs/compliance/cis/cis_microsoft365.py diff --git a/prowler/__main__.py b/prowler/__main__.py index 8b4e1972c23..d1aee094eb5 100644 --- a/prowler/__main__.py +++ b/prowler/__main__.py @@ -51,6 +51,7 @@ from prowler.lib.outputs.compliance.cis.cis_azure import AzureCIS from prowler.lib.outputs.compliance.cis.cis_gcp import GCPCIS from prowler.lib.outputs.compliance.cis.cis_kubernetes import KubernetesCIS +from prowler.lib.outputs.compliance.cis.cis_microsoft365 import Microsoft365CIS from prowler.lib.outputs.compliance.compliance import display_compliance_table from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS from prowler.lib.outputs.compliance.ens.ens_azure import AzureENS @@ -312,7 +313,6 @@ def prowler(): if "SLACK_API_TOKEN" in environ and ( "SLACK_CHANNEL_NAME" in environ or "SLACK_CHANNEL_ID" in environ ): - token = environ["SLACK_API_TOKEN"] channel = ( environ["SLACK_CHANNEL_NAME"] @@ -634,6 +634,36 @@ def prowler(): generated_outputs["compliance"].append(generic_compliance) generic_compliance.batch_write_data_to_file() + elif provider == "microsoft365": + for compliance_name in input_compliance_frameworks: + if compliance_name.startswith("cis_"): + # Generate CIS Finding Object + filename = ( + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" + ) + cis = Microsoft365CIS( + findings=finding_outputs, + compliance=bulk_compliance_frameworks[compliance_name], + create_file_descriptor=True, + file_path=filename, + ) + generated_outputs["compliance"].append(cis) + cis.batch_write_data_to_file() + else: + filename = ( + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" + ) + generic_compliance = GenericCompliance( + findings=finding_outputs, + compliance=bulk_compliance_frameworks[compliance_name], + create_file_descriptor=True, + file_path=filename, + ) + generated_outputs["compliance"].append(generic_compliance) + generic_compliance.batch_write_data_to_file() + # AWS Security Hub Integration if provider == "aws": # Send output to S3 if needed (-B / -D) for all the output formats diff --git a/prowler/compliance/microsoft365/__init__.py b/prowler/compliance/microsoft365/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/config/config.py b/prowler/config/config.py index bf018abb015..acdb4460287 100644 --- a/prowler/config/config.py +++ b/prowler/config/config.py @@ -28,6 +28,7 @@ class Provider(str, Enum): GCP = "gcp" AZURE = "azure" KUBERNETES = "kubernetes" + MICROSOFT365 = "microsoft365" # Compliance diff --git a/prowler/lib/cli/parser.py b/prowler/lib/cli/parser.py index fc5b2bc3ba7..cff89401340 100644 --- a/prowler/lib/cli/parser.py +++ b/prowler/lib/cli/parser.py @@ -26,7 +26,7 @@ def __init__(self): self.parser = argparse.ArgumentParser( prog="prowler", formatter_class=RawTextHelpFormatter, - usage="prowler [-h] [--version] {aws,azure,gcp,kubernetes,dashboard} ...", + usage="prowler [-h] [--version] {aws,azure,gcp,kubernetes,microsoft365,dashboard} ...", epilog=""" Available Cloud Providers: {aws,azure,gcp,kubernetes} @@ -73,7 +73,7 @@ def __init__(self): # Init Providers Arguments init_providers_parser(self) - # Dahboard Parser + # Dashboard Parser init_dashboard_parser(self) def parse(self, args=None) -> argparse.Namespace: diff --git a/prowler/lib/outputs/compliance/cis/cis_microsoft365.py b/prowler/lib/outputs/compliance/cis/cis_microsoft365.py new file mode 100644 index 00000000000..c2698295267 --- /dev/null +++ b/prowler/lib/outputs/compliance/cis/cis_microsoft365.py @@ -0,0 +1,99 @@ +from prowler.lib.check.compliance_models import Compliance +from prowler.lib.outputs.compliance.cis.models import Microsoft365CISModel +from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput +from prowler.lib.outputs.finding import Finding + + +class Microsoft365CIS(ComplianceOutput): + """ + This class represents the Azure CIS compliance output. + + Attributes: + - _data (list): A list to store transformed data from findings. + - _file_descriptor (TextIOWrapper): A file descriptor to write data to a file. + + Methods: + - transform: Transforms findings into Azure CIS compliance format. + """ + + def transform( + self, + findings: list[Finding], + compliance: Compliance, + compliance_name: str, + ) -> None: + """ + Transforms a list of findings into Azure CIS compliance format. + + Parameters: + - findings (list): A list of findings. + - compliance (Compliance): A compliance model. + - compliance_name (str): The name of the compliance model. + + Returns: + - None + """ + for finding in findings: + # Get the compliance requirements for the finding + finding_requirements = finding.compliance.get(compliance_name, []) + for requirement in compliance.Requirements: + if requirement.Id in finding_requirements: + for attribute in requirement.Attributes: + compliance_row = Microsoft365CISModel( + Provider=finding.provider, + Description=compliance.Description, + SubscriptionId=finding.account_uid, + Location=finding.region, + AssessmentDate=str(finding.timestamp), + Requirements_Id=requirement.Id, + Requirements_Description=requirement.Description, + Requirements_Attributes_Section=attribute.Section, + Requirements_Attributes_Profile=attribute.Profile, + Requirements_Attributes_AssessmentStatus=attribute.AssessmentStatus, + Requirements_Attributes_Description=attribute.Description, + Requirements_Attributes_RationaleStatement=attribute.RationaleStatement, + Requirements_Attributes_ImpactStatement=attribute.ImpactStatement, + Requirements_Attributes_RemediationProcedure=attribute.RemediationProcedure, + Requirements_Attributes_AuditProcedure=attribute.AuditProcedure, + Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation, + Requirements_Attributes_DefaultValue=attribute.DefaultValue, + Requirements_Attributes_References=attribute.References, + Status=finding.status, + StatusExtended=finding.status_extended, + ResourceId=finding.resource_uid, + ResourceName=finding.resource_name, + CheckId=finding.check_id, + Muted=finding.muted, + ) + self._data.append(compliance_row) + # Add manual requirements to the compliance output + for requirement in compliance.Requirements: + if not requirement.Checks: + for attribute in requirement.Attributes: + compliance_row = Microsoft365CISModel( + Provider=compliance.Provider.lower(), + Description=compliance.Description, + SubscriptionId="", + Location="", + AssessmentDate=str(finding.timestamp), + Requirements_Id=requirement.Id, + Requirements_Description=requirement.Description, + Requirements_Attributes_Section=attribute.Section, + Requirements_Attributes_Profile=attribute.Profile, + Requirements_Attributes_AssessmentStatus=attribute.AssessmentStatus, + Requirements_Attributes_Description=attribute.Description, + Requirements_Attributes_RationaleStatement=attribute.RationaleStatement, + Requirements_Attributes_ImpactStatement=attribute.ImpactStatement, + Requirements_Attributes_RemediationProcedure=attribute.RemediationProcedure, + Requirements_Attributes_AuditProcedure=attribute.AuditProcedure, + Requirements_Attributes_AdditionalInformation=attribute.AdditionalInformation, + Requirements_Attributes_DefaultValue=attribute.DefaultValue, + Requirements_Attributes_References=attribute.References, + Status="MANUAL", + StatusExtended="Manual check", + ResourceId="manual_check", + ResourceName="Manual check", + CheckId="manual", + Muted=False, + ) + self._data.append(compliance_row) diff --git a/prowler/lib/outputs/compliance/cis/models.py b/prowler/lib/outputs/compliance/cis/models.py index d8e1889d529..6b2abbd6b9f 100644 --- a/prowler/lib/outputs/compliance/cis/models.py +++ b/prowler/lib/outputs/compliance/cis/models.py @@ -62,6 +62,37 @@ class AzureCISModel(BaseModel): Muted: bool +class Microsoft365CISModel(BaseModel): + """ + Microsoft365CISModel generates a finding's output in Microsoft365 CIS Compliance format. + """ + + Provider: str + Description: str + SubscriptionId: str + Location: str + AssessmentDate: str + Requirements_Id: str + Requirements_Description: str + Requirements_Attributes_Section: str + Requirements_Attributes_Profile: str + Requirements_Attributes_AssessmentStatus: str + Requirements_Attributes_Description: str + Requirements_Attributes_RationaleStatement: str + Requirements_Attributes_ImpactStatement: str + Requirements_Attributes_RemediationProcedure: str + Requirements_Attributes_AuditProcedure: str + Requirements_Attributes_AdditionalInformation: str + Requirements_Attributes_DefaultValue: str + Requirements_Attributes_References: str + Status: str + StatusExtended: str + ResourceId: str + ResourceName: str + CheckId: str + Muted: bool + + class GCPCISModel(BaseModel): """ GCPCISModel generates a finding's output in GCP CIS Compliance format. diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index 9abac8ff9a7..61cf0b6c76f 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -213,6 +213,7 @@ def init_global_provider(arguments: Namespace) -> None: ) elif "microsoft365" in provider_class_name.lower(): provider_class( + region=arguments.region, config_path=arguments.config_file, mutelist_path=arguments.mutelist_file, fixer_config=fixer_config, diff --git a/prowler/providers/microsoft365/lib/arguments/arguments.py b/prowler/providers/microsoft365/lib/arguments/arguments.py index 12a8707264d..afa5a2cf047 100644 --- a/prowler/providers/microsoft365/lib/arguments/arguments.py +++ b/prowler/providers/microsoft365/lib/arguments/arguments.py @@ -11,7 +11,7 @@ def init_parser(self): # Regions microsoft365_regions_subparser = microsoft365_parser.add_argument_group("Regions") microsoft365_regions_subparser.add_argument( - "--microsoft365-region", + "--region", nargs="?", default="Microsoft365Global", type=validate_microsoft365_region, diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index c285c2d6ac0..7d723694eb8 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -38,9 +38,6 @@ Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError, Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError, ) -from prowler.providers.microsoft365.lib.arguments.arguments import ( - validate_microsoft365_region, -) from prowler.providers.microsoft365.lib.mutelist.mutelist import Microsoft365Mutelist from prowler.providers.microsoft365.lib.regions.microsoft365_regions import ( get_regions_config, @@ -143,7 +140,6 @@ def __init__( # Set up the Microsoft365 session self._session = self.setup_session( microsoft365_credentials, - self._region_config, ) # Set up the identity @@ -246,7 +242,6 @@ def setup_region_config(region): """ try: - validate_microsoft365_region(region) config = get_regions_config(region) return Microsoft365RegionConfig( @@ -299,7 +294,6 @@ def print_credentials(self): @staticmethod def setup_session( microsoft365_credentials: dict, - region_config: Microsoft365RegionConfig, ): """Returns the Microsoft365 credentials object. diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py index b146569488a..361f9f2a856 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py @@ -8,7 +8,7 @@ class admincenter_users_admins_reduced_license_footprint(Check): def execute(self) -> Check_Report_Microsoft365: findings = [] allowed_licenses = ["AAD_PREMIUM", "AAD_PREMIUM_P2"] - for user_principal_name, user in admincenter_client.users.items(): + for user in admincenter_client.users.values(): admin_roles = [ role for role in user.directory_roles diff --git a/tests/lib/cli/parser_test.py b/tests/lib/cli/parser_test.py index 3f92ca2d57c..16b322ac9c6 100644 --- a/tests/lib/cli/parser_test.py +++ b/tests/lib/cli/parser_test.py @@ -16,13 +16,11 @@ # capsys # https://docs.pytest.org/en/7.1.x/how-to/capture-stdout-stderr.html -prowler_default_usage_error = ( - "usage: prowler [-h] [--version] {aws,azure,gcp,kubernetes,dashboard} ..." -) +prowler_default_usage_error = "usage: prowler [-h] [--version] {aws,azure,gcp,kubernetes,microsoft365,dashboard} ..." def mock_get_available_providers(): - return ["aws", "azure", "gcp", "kubernetes"] + return ["aws", "azure", "gcp", "kubernetes", "microsoft365"] @pytest.mark.arg_parser From a2a5691ded60cfbfd9dc3dd57d3eb17ac99b3d08 Mon Sep 17 00:00:00 2001 From: MrCloudSec Date: Tue, 21 Jan 2025 16:55:42 -0500 Subject: [PATCH 27/44] chore: improve check --- ..._users_admins_reduced_license_footprint.py | 14 ++-- ...s_admins_reduced_license_footprint_test.py | 64 +++++++++++-------- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py index 361f9f2a856..4302549dbe7 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py @@ -9,11 +9,13 @@ def execute(self) -> Check_Report_Microsoft365: findings = [] allowed_licenses = ["AAD_PREMIUM", "AAD_PREMIUM_P2"] for user in admincenter_client.users.values(): - admin_roles = [ - role - for role in user.directory_roles - if "Administrator" in role or "Globar Reader" in role - ] + admin_roles = ", ".join( + [ + role + for role in user.directory_roles + if "Administrator" in role or "Globar Reader" in role + ] + ) if admin_roles: report = Check_Report_Microsoft365(self.metadata()) @@ -22,7 +24,7 @@ def execute(self) -> Check_Report_Microsoft365: report.tenant_id = admincenter_client.audited_tenant report.tenant_domain = admincenter_client.audited_domain report.status = "FAIL" - report.status_extended = f"User {user.name} has administrative roles {admin_roles} and an invalid license {user.license}." + report.status_extended = f"User {user.name} has administrative roles {admin_roles} and an invalid license {user.license if user.license else ''}." if user.license in allowed_licenses: report.status = "PASS" diff --git a/tests/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint_test.py b/tests/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint_test.py index 91a5453b2a3..554cd511694 100644 --- a/tests/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint_test.py +++ b/tests/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint_test.py @@ -13,12 +13,15 @@ def test_admincenter_no_users(self): admincenter_client.audited_tenant = "audited_tenant" admincenter_client.audited_domain = DOMAIN - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_microsoft365_provider(), - ), mock.patch( - "prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", - new=admincenter_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", + new=admincenter_client, + ), ): from prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint import ( admincenter_users_admins_reduced_license_footprint, @@ -35,12 +38,15 @@ def test_admincenter_user_no_admin(self): admincenter_client.audited_tenant = "audited_tenant" admincenter_client.audited_domain = DOMAIN - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_microsoft365_provider(), - ), mock.patch( - "prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", - new=admincenter_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", + new=admincenter_client, + ), ): from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( User, @@ -69,12 +75,15 @@ def test_admincenter_user_admin_compliant_license(self): admincenter_client.audited_tenant = "audited_tenant" admincenter_client.audited_domain = DOMAIN - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_microsoft365_provider(), - ), mock.patch( - "prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", - new=admincenter_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", + new=admincenter_client, + ), ): from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( User, @@ -100,7 +109,7 @@ def test_admincenter_user_admin_compliant_license(self): assert result[0].status == "PASS" assert ( result[0].status_extended - == "User User1 has administrative roles ['Global Administrator'] and a valid license: AAD_PREMIUM." + == "User User1 has administrative roles Global Administrator and a valid license: AAD_PREMIUM." ) assert result[0].resource_name == "User1" assert result[0].resource_id == id_user1 @@ -110,12 +119,15 @@ def test_admincenter_user_admin_non_compliant_license(self): admincenter_client.audited_tenant = "audited_tenant" admincenter_client.audited_domain = DOMAIN - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_microsoft365_provider(), - ), mock.patch( - "prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", - new=admincenter_client, + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_microsoft365_provider(), + ), + mock.patch( + "prowler.providers.microsoft365.services.admincenter.admincenter_users_admins_reduced_license_footprint.admincenter_users_admins_reduced_license_footprint.admincenter_client", + new=admincenter_client, + ), ): from prowler.providers.microsoft365.services.admincenter.admincenter_service import ( User, @@ -141,7 +153,7 @@ def test_admincenter_user_admin_non_compliant_license(self): assert result[0].status == "FAIL" assert ( result[0].status_extended - == "User User1 has administrative roles ['Global Administrator'] and an invalid license O365 BUSINESS." + == "User User1 has administrative roles Global Administrator and an invalid license O365 BUSINESS." ) assert result[0].resource_name == "User1" assert result[0].resource_id == id_user1 From 93be3e1cc2fb7b9732c938c689a154dff8d335b9 Mon Sep 17 00:00:00 2001 From: HugoPBrito Date: Wed, 22 Jan 2025 14:13:36 +0100 Subject: [PATCH 28/44] feat: add new authentication flow --- .../microsoft365/exceptions/exceptions.py | 7 + .../microsoft365/lib/arguments/arguments.py | 22 ++ .../microsoft365/microsoft365_provider.py | 216 +++++++++++------- 3 files changed, 167 insertions(+), 78 deletions(-) diff --git a/prowler/providers/microsoft365/exceptions/exceptions.py b/prowler/providers/microsoft365/exceptions/exceptions.py index 8be172b7f81..024cb91020c 100644 --- a/prowler/providers/microsoft365/exceptions/exceptions.py +++ b/prowler/providers/microsoft365/exceptions/exceptions.py @@ -220,3 +220,10 @@ def __init__(self, file=None, original_exception=None, message=None): super().__init__( 6016, file=file, original_exception=original_exception, message=message ) + + +class Microsoft365InteractiveBrowserCredentialError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6017, file=file, original_exception=original_exception, message=message + ) diff --git a/prowler/providers/microsoft365/lib/arguments/arguments.py b/prowler/providers/microsoft365/lib/arguments/arguments.py index afa5a2cf047..edc9a9da30c 100644 --- a/prowler/providers/microsoft365/lib/arguments/arguments.py +++ b/prowler/providers/microsoft365/lib/arguments/arguments.py @@ -8,6 +8,28 @@ def init_parser(self): parents=[self.common_providers_parser], help="Microsoft365 Provider", ) + # Authentication Modes + microsoft365_auth_subparser = microsoft365_parser.add_argument_group( + "Authentication Modes" + ) + microsoft365_auth_modes_group = ( + microsoft365_auth_subparser.add_mutually_exclusive_group() + ) + microsoft365_auth_modes_group.add_argument( + "--m365-app-env-auth", + action="store_true", + help="Use application authentication with environment variables to log in against Microsoft365", + ) + microsoft365_auth_modes_group.add_argument( + "--m365-cli-auth", + action="store_true", + help="Use Azure CLI authentication to log in against Microsoft365", + ) + microsoft365_auth_modes_group.add_argument( + "--m365-browser-auth", + action="store_true", + help="Use interactive browser authentication to log in against Microsoft365", + ) # Regions microsoft365_regions_subparser = microsoft365_parser.add_argument_group("Regions") microsoft365_regions_subparser.add_argument( diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index 7d723694eb8..7e5046465d3 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -6,7 +6,11 @@ from uuid import UUID from azure.core.exceptions import ClientAuthenticationError, HttpResponseError -from azure.identity import ClientSecretCredential, CredentialUnavailableError +from azure.identity import ( + ClientSecretCredential, + DefaultAzureCredential, + InteractiveBrowserCredential, +) from colorama import Fore, Style from msal import ConfidentialClientApplication from msgraph import GraphServiceClient @@ -29,6 +33,7 @@ Microsoft365EnvironmentVariableError, Microsoft365GetTokenIdentityError, Microsoft365HTTPResponseError, + Microsoft365InteractiveBrowserCredentialError, Microsoft365InvalidProviderIdError, Microsoft365NotValidClientIdError, Microsoft365NotValidClientSecretError, @@ -38,6 +43,9 @@ Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError, Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError, ) +from prowler.providers.microsoft365.lib.arguments.arguments import ( + validate_microsoft365_region, +) from prowler.providers.microsoft365.lib.mutelist.mutelist import Microsoft365Mutelist from prowler.providers.microsoft365.lib.regions.microsoft365_regions import ( get_regions_config, @@ -81,7 +89,7 @@ class Microsoft365Provider(Provider): """ _type: str = "microsoft365" - _session: ClientSecretCredential + _session: DefaultAzureCredential # Must be used besides being named for Azure _identity: Microsoft365IdentityInfo _audit_config: dict _region_config: Microsoft365RegionConfig @@ -91,55 +99,53 @@ class Microsoft365Provider(Provider): def __init__( self, - tenant_id: str = None, + # Authentication credentials + tenant_id: str = "", + client_id: str = "", + client_secret: str = "", + # Authentication methods + m365_cli_auth: bool = False, + m365_app_auth: bool = False, + m365_browser_auth: bool = False, + # Provider configuration region: str = "Microsoft365Global", - client_id: str = None, - client_secret: str = None, - config_content: dict = None, config_path: str = None, + config_content: dict = None, mutelist_path: str = None, mutelist_content: dict = None, fixer_config: dict = {}, ): """ - Initializes the Microsoft365 provider. + Microsoft365 Provider constructor Args: - tenant_id (str): The Microsoft365 Active Directory tenant ID. - region (str): The Microsoft365 region. - client_id (str): The Microsoft365 client ID. - client_secret (str): The Microsoft365 client secret. - config_path (str): The path to the configuration file. - config_content (dict): The configuration content. - fixer_config (dict): The fixer configuration. - mutelist_path (str): The path to the mutelist file. - mutelist_content (dict): The mutelist content. - - Returns: - None - - Raises: - Microsoft365ArgumentTypeValidationError: If there is an error in the argument type validation. - Microsoft365SetUpRegionConfigError: If there is an error in setting up the region configuration. - Microsoft365ConfigCredentialsError: If there is an error in configuring the Microsoft365 credentials from a dictionary. - Microsoft365GetTokenIdentityError: If there is an error in getting the token from the Microsoft365 identity. - Microsoft365HTTPResponseError: If there is an HTTP response error. + tenant_id (str): Microsoft365 tenant ID + client_id (str): Microsoft365 client ID + client_secret (str): Microsoft365 client secret + m365_cli_auth (bool): Use Azure CLI authentication + m365_app_auth (bool): Use environment variables authentication + m365_browser_auth (bool): Use browser authentication + region (str): Microsoft365 region + config_path (str): Path to the audit configuration file + config_content (dict): Audit configuration content + mutelist_path (str): Path to the mutelist file + mutelist_content (dict): Mutelist content + fixer_config (dict): Fixer configuration """ logger.info("Setting Microsoft365 provider ...") logger.info("Checking if region is different than default one") self._region_config = self.setup_region_config(region) - # Get the dict from the static credentials - microsoft365_credentials = None - if tenant_id and client_id and client_secret: - microsoft365_credentials = self.validate_static_credentials( - tenant_id=tenant_id, client_id=client_id, client_secret=client_secret - ) - # Set up the Microsoft365 session self._session = self.setup_session( - microsoft365_credentials, + m365_cli_auth, + m365_app_auth, + m365_browser_auth, + tenant_id, + client_id, + client_secret, + self._region_config, ) # Set up the identity @@ -242,6 +248,7 @@ def setup_region_config(region): """ try: + validate_microsoft365_region(region) config = get_regions_config(region) return Microsoft365RegionConfig( @@ -291,16 +298,30 @@ def print_credentials(self): # TODO: setup_session or setup_credentials? # This should be setup_credentials, since it is setting up the credentials for the provider - @staticmethod def setup_session( - microsoft365_credentials: dict, + self, + m365_cli_auth: bool, + m365_app_auth: bool, + m365_browser_auth: bool, + tenant_id: str, + client_id: str, + client_secret: str, + region_config: Microsoft365RegionConfig, ): """Returns the Microsoft365 credentials object. Set up the Microsoft365 session with the specified authentication method. Args: - app_env_auth (bool): Flag indicating whether to use application authentication with environment variables. + m365_cli_auth (bool): Flag indicating whether to use Azure CLI authentication. + m365_app_auth (bool): Flag indicating whether to use application authentication with environment variables. + m365_browser_auth (bool): Flag indicating whether to use interactive browser authentication. + tenant_id (str): The Microsoft365 Active Directory tenant ID. + m365_credentials (dict): The Microsoft365 configuration object. It contains the following keys: + - tenant_id: The Microsoft365 Active Directory tenant ID. + - client_id: The Microsoft365 client ID. + - client_secret: The Microsoft365 client secret + region_config (Microsoft365RegionConfig): The region configuration object. Returns: credentials: The Microsoft365 credentials object. @@ -309,47 +330,83 @@ def setup_session( Exception: If failed to retrieve Microsoft365 credentials. """ + microsoft365_credentials = None + try: - Microsoft365Provider.check_application_creds_env_vars() - credentials = ClientSecretCredential( - client_id=getenv("APP_CLIENT_ID"), - tenant_id=getenv("APP_TENANT_ID"), - client_secret=getenv("APP_CLIENT_SECRET"), - ) - except Microsoft365EnvironmentVariableError as environment_credentials_error: - logger.critical( - f"{environment_credentials_error.__class__.__name__}[{environment_credentials_error.__traceback__.tb_lineno}] -- {environment_credentials_error}" - ) - raise environment_credentials_error - try: - if microsoft365_credentials: - try: - credentials = ClientSecretCredential( - tenant_id=microsoft365_credentials["tenant_id"], - client_id=microsoft365_credentials["client_id"], - client_secret=microsoft365_credentials["client_secret"], - ) - return credentials - except ClientAuthenticationError as error: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - raise Microsoft365ClientAuthenticationError( - file=os.path.basename(__file__), original_exception=error - ) - except CredentialUnavailableError as error: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - raise Microsoft365CredentialsUnavailableError( - file=os.path.basename(__file__), original_exception=error - ) - except Exception as error: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - raise Microsoft365ConfigCredentialsError( - file=os.path.basename(__file__), original_exception=error + # Try to get credentials from cli arguments + if m365_app_auth and tenant_id and client_id and client_secret: + self.validate_arguments(tenant_id, client_id, client_secret) + self._auth_method = "Client Secret Credentials" + microsoft365_credentials = { + "tenant_id": tenant_id, + "client_id": client_id, + "client_secret": client_secret, + } + elif m365_cli_auth and tenant_id: + self._auth_method = "CLI Authentication" + microsoft365_credentials = { + "tenant_id": tenant_id, + } + # If not credentials are provided, try to get them from environment variables + else: + microsoft365_credentials = ClientSecretCredential( + tenant_id=getenv("APP_TENANT_ID", ""), + client_id=getenv("APP_CLIENT_ID", ""), + client_secret=getenv("APP_CLIENT_SECRET", ""), + ) + if not m365_browser_auth and (m365_app_auth or m365_cli_auth): + # Since the authentication method to be used will come as True, we have to negate it since + # DefaultAzureCredential sets just one authentication method, excluding the others + try: + microsoft365_credentials = DefaultAzureCredential( + exclude_environment_credential=True, + exclude_cli_credential=not m365_cli_auth, + # M365 does not support managed identity authentication + exclude_managed_identity_credential=True, + # Azure Auth using Visual Studio is not supported + exclude_visual_studio_code_credential=True, + # Azure Auth using Shared Token Cache is not supported + exclude_shared_token_cache_credential=True, + # Azure Auth using PowerShell is not supported + exclude_powershell_credential=True, + # set Authority of a Microsoft Entra endpoint + authority=region_config.authority, + ) + except ClientAuthenticationError as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365ClientAuthenticationError( + file=os.path.basename(__file__), original_exception=error + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365ConfigCredentialsError( + file=os.path.basename(__file__), original_exception=error + ) + # Browser auth creds cannot be set with DefaultAzureCredentials() + elif m365_browser_auth: + try: + microsoft365_credentials = InteractiveBrowserCredential( + tenant_id=tenant_id + ) + self._auth_method = "Browser Authentication" + except Exception as error: + logger.critical( + "Failed to retrieve azure credentials using browser authentication" + ) + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365InteractiveBrowserCredentialError( + file=os.path.basename(__file__), original_exception=error + ) + if not microsoft365_credentials: + raise Microsoft365EnvironmentVariableError( + file=os.path.basename(__file__), + message="No authentication method selected and no environment variables were found.", ) except Exception as error: logger.critical("Failed to retrieve Microsoft365 credentials") @@ -359,7 +416,7 @@ def setup_session( raise Microsoft365SetUpSessionError( file=os.path.basename(__file__), original_exception=error ) - return credentials + return microsoft365_credentials @staticmethod def test_connection( @@ -541,15 +598,18 @@ def check_application_creds_env_vars(): logger.info( "Microsoft365 provider: checking service principal environment variables ..." ) + all_present = True for env_var in ["APP_CLIENT_ID", "APP_TENANT_ID", "APP_CLIENT_SECRET"]: if not getenv(env_var): logger.critical( f"Microsoft365 provider: Missing environment variable {env_var} needed to authenticate against Microsoft365" ) + all_present = False raise Microsoft365EnvironmentVariableError( file=os.path.basename(__file__), message=f"Missing environment variable {env_var} required to authenticate.", ) + return all_present def setup_identity( self, From 2522de899a640c373d64585975e6158cbd4b3b11 Mon Sep 17 00:00:00 2001 From: HugoPBrito Date: Wed, 22 Jan 2025 21:08:47 +0100 Subject: [PATCH 29/44] feat: added cli and browser auth methods --- prowler/providers/common/provider.py | 4 + .../microsoft365/exceptions/exceptions.py | 32 +- .../microsoft365/lib/arguments/arguments.py | 14 +- .../microsoft365/microsoft365_provider.py | 382 +++++++++++------- 4 files changed, 280 insertions(+), 152 deletions(-) diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index 61cf0b6c76f..01d1bfbf793 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -217,6 +217,10 @@ def init_global_provider(arguments: Namespace) -> None: config_path=arguments.config_file, mutelist_path=arguments.mutelist_file, fixer_config=fixer_config, + m365_env_app_auth=arguments.m365_env_app_auth, + m365_cli_auth=arguments.m365_cli_auth, + m365_browser_auth=arguments.m365_browser_auth, + tenant_id=arguments.tenant_id, ) except TypeError as error: diff --git a/prowler/providers/microsoft365/exceptions/exceptions.py b/prowler/providers/microsoft365/exceptions/exceptions.py index 024cb91020c..8d3b49c796f 100644 --- a/prowler/providers/microsoft365/exceptions/exceptions.py +++ b/prowler/providers/microsoft365/exceptions/exceptions.py @@ -222,8 +222,38 @@ def __init__(self, file=None, original_exception=None, message=None): ) -class Microsoft365InteractiveBrowserCredentialError(Microsoft365CredentialsError): +class Microsoft365DefaultAzureCredentialError(Microsoft365CredentialsError): def __init__(self, file=None, original_exception=None, message=None): super().__init__( 6017, file=file, original_exception=original_exception, message=message ) + + +class Microsoft365InteractiveBrowserCredentialError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6018, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365BrowserAuthNoTenantIDError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6019, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365BrowserAuthNoFlagError(Microsoft365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6020, file=file, original_exception=original_exception, message=message + ) + + +class Microsoft365NotTenantIdButClientIdAndClienSecretError( + Microsoft365CredentialsError +): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6021, file=file, original_exception=original_exception, message=message + ) diff --git a/prowler/providers/microsoft365/lib/arguments/arguments.py b/prowler/providers/microsoft365/lib/arguments/arguments.py index edc9a9da30c..0a8112ec4c3 100644 --- a/prowler/providers/microsoft365/lib/arguments/arguments.py +++ b/prowler/providers/microsoft365/lib/arguments/arguments.py @@ -16,20 +16,26 @@ def init_parser(self): microsoft365_auth_subparser.add_mutually_exclusive_group() ) microsoft365_auth_modes_group.add_argument( - "--m365-app-env-auth", + "--m365-cli-auth", action="store_true", - help="Use application authentication with environment variables to log in against Microsoft365", + help="Use Azure CLI authentication to log in against Microsoft365", ) microsoft365_auth_modes_group.add_argument( - "--m365-cli-auth", + "--m365-env-app-auth", action="store_true", - help="Use Azure CLI authentication to log in against Microsoft365", + help="Use application authentication with environment variables to log in against Microsoft365", ) microsoft365_auth_modes_group.add_argument( "--m365-browser-auth", action="store_true", help="Use interactive browser authentication to log in against Microsoft365", ) + microsoft365_parser.add_argument( + "--tenant-id", + nargs="?", + default=None, + help="Microsoft365 Tenant ID to be used with --browser-auth option", + ) # Regions microsoft365_regions_subparser = microsoft365_parser.add_argument_group("Regions") microsoft365_regions_subparser.add_argument( diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index 7e5046465d3..f09e6267258 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -8,6 +8,7 @@ from azure.core.exceptions import ClientAuthenticationError, HttpResponseError from azure.identity import ( ClientSecretCredential, + CredentialUnavailableError, DefaultAzureCredential, InteractiveBrowserCredential, ) @@ -26,15 +27,20 @@ from prowler.providers.common.provider import Provider from prowler.providers.microsoft365.exceptions.exceptions import ( Microsoft365ArgumentTypeValidationError, + Microsoft365BrowserAuthNoFlagError, + Microsoft365BrowserAuthNoTenantIDError, Microsoft365ClientAuthenticationError, Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError, Microsoft365ConfigCredentialsError, Microsoft365CredentialsUnavailableError, + Microsoft365DefaultAzureCredentialError, Microsoft365EnvironmentVariableError, Microsoft365GetTokenIdentityError, Microsoft365HTTPResponseError, Microsoft365InteractiveBrowserCredentialError, Microsoft365InvalidProviderIdError, + Microsoft365NoAuthenticationMethodError, + Microsoft365NotTenantIdButClientIdAndClienSecretError, Microsoft365NotValidClientIdError, Microsoft365NotValidClientSecretError, Microsoft365NotValidTenantIdError, @@ -99,57 +105,84 @@ class Microsoft365Provider(Provider): def __init__( self, - # Authentication credentials - tenant_id: str = "", - client_id: str = "", - client_secret: str = "", - # Authentication methods - m365_cli_auth: bool = False, - m365_app_auth: bool = False, - m365_browser_auth: bool = False, - # Provider configuration + m365_env_app_auth: bool, + m365_cli_auth: bool, + m365_browser_auth: bool, + tenant_id: str = None, + client_id: str = None, + client_secret: str = None, region: str = "Microsoft365Global", - config_path: str = None, config_content: dict = None, + config_path: str = None, mutelist_path: str = None, mutelist_content: dict = None, fixer_config: dict = {}, ): """ - Microsoft365 Provider constructor + Initializes the Microsoft365 provider. Args: - tenant_id (str): Microsoft365 tenant ID - client_id (str): Microsoft365 client ID - client_secret (str): Microsoft365 client secret - m365_cli_auth (bool): Use Azure CLI authentication - m365_app_auth (bool): Use environment variables authentication - m365_browser_auth (bool): Use browser authentication - region (str): Microsoft365 region - config_path (str): Path to the audit configuration file - config_content (dict): Audit configuration content - mutelist_path (str): Path to the mutelist file - mutelist_content (dict): Mutelist content - fixer_config (dict): Fixer configuration + tenant_id (str): The Microsoft365 Active Directory tenant ID. + region (str): The Microsoft365 region. + client_id (str): The Microsoft365 client ID. + client_secret (str): The Microsoft365 client secret. + config_path (str): The path to the configuration file. + config_content (dict): The configuration content. + fixer_config (dict): The fixer configuration. + mutelist_path (str): The path to the mutelist file. + mutelist_content (dict): The mutelist content. + + Returns: + None + + Raises: + Microsoft365ArgumentTypeValidationError: If there is an error in the argument type validation. + Microsoft365SetUpRegionConfigError: If there is an error in setting up the region configuration. + Microsoft365ConfigCredentialsError: If there is an error in configuring the Microsoft365 credentials from a dictionary. + Microsoft365GetTokenIdentityError: If there is an error in getting the token from the Microsoft365 identity. + Microsoft365HTTPResponseError: If there is an HTTP response error. """ logger.info("Setting Microsoft365 provider ...") + logger.info("Checking if any credentials mode is set ...") + + # Validate the authentication arguments + self.validate_arguments( + m365_cli_auth, + m365_env_app_auth, + m365_browser_auth, + tenant_id, + client_id, + client_secret, + ) + logger.info("Checking if region is different than default one") self._region_config = self.setup_region_config(region) + # Get the dict from the static credentials + microsoft365_credentials = None + if tenant_id and client_id and client_secret: + microsoft365_credentials = self.validate_static_credentials( + tenant_id=tenant_id, client_id=client_id, client_secret=client_secret + ) + # Set up the Microsoft365 session self._session = self.setup_session( m365_cli_auth, - m365_app_auth, + m365_env_app_auth, m365_browser_auth, tenant_id, - client_id, - client_secret, + microsoft365_credentials, self._region_config, ) # Set up the identity - self._identity = self.setup_identity() + self._identity = self.setup_identity( + m365_cli_auth, + m365_env_app_auth, + m365_browser_auth, + client_id, + ) # Audit Config if config_content: @@ -213,6 +246,9 @@ def mutelist(self) -> Microsoft365Mutelist: @staticmethod def validate_arguments( + m365_cli_auth: bool, + m365_env_app_auth: bool, + m365_browser_auth: bool, tenant_id: str, client_id: str, client_secret: str, @@ -221,19 +257,39 @@ def validate_arguments( Validates the authentication arguments for the Microsoft365 provider. Args: + m365_cli_auth (bool): Flag indicating whether Azure CLI authentication is enabled. + m365_env_app_auth (bool): Flag indicating whether application authentication with environment variables is enabled. + m365_browser_auth (bool): Flag indicating whether browser authentication is enabled. tenant_id (str): The Microsoft365 Tenant ID. client_id (str): The Microsoft365 Client ID. client_secret (str): The Microsoft365 Client Secret. Raises: - + Microsoft365BrowserAuthNoTenantIDError: If browser authentication is enabled but the tenant ID is not found. """ - if not client_id or not client_secret or not tenant_id: - raise Microsoft365IdentityInfo( - file=os.path.basename(__file__), - message="Tenant Id is required for Microsoft365 static credentials. Make sure you are using the correct credentials.", - ) + if not client_id and not client_secret: + if not m365_browser_auth and tenant_id: + raise Microsoft365BrowserAuthNoFlagError( + file=os.path.basename(__file__), + message="Microsoft365 Tenant ID (--m365-browser-auth) is required for browser authentication mode", + ) + elif not m365_cli_auth and not m365_env_app_auth and not m365_browser_auth: + raise Microsoft365NoAuthenticationMethodError( + file=os.path.basename(__file__), + message="Microsoft365 provider requires at least one authentication method set: [--m365-cli-auth | --m365-env-app-auth | --m365-browser-auth]", + ) + elif m365_browser_auth and not tenant_id: + raise Microsoft365BrowserAuthNoTenantIDError( + file=os.path.basename(__file__), + message="Microsoft365 Tenant ID (--tenant-id) is required for browser authentication mode", + ) + else: + if not tenant_id: + raise Microsoft365NotTenantIdButClientIdAndClienSecretError( + file=os.path.basename(__file__), + message="Tenant Id is required for Microsoft365 static credentials. Make sure you are using the correct credentials.", + ) @staticmethod def setup_region_config(region): @@ -288,7 +344,7 @@ def print_credentials(self): """ report_lines = [ f"Microsoft365 Region: {Fore.YELLOW}{self.region_config.name}{Style.RESET_ALL}", - f"Microsoft365 Tenant Domain: {Fore.YELLOW}{self.identity.tenant_domain}{Style.RESET_ALL} Microsoft365 Tenant ID: {Fore.YELLOW}{self._identity.tenant_id}{Style.RESET_ALL}", + f"Microsoft365 Tenant Domain: {Fore.YELLOW}{self._identity.tenant_domain}{Style.RESET_ALL} Microsoft365 Tenant ID: {Fore.YELLOW}{self._identity.tenant_id}{Style.RESET_ALL}", f"Microsoft365 Identity Type: {Fore.YELLOW}{self._identity.identity_type}{Style.RESET_ALL} Microsoft365 Identity ID: {Fore.YELLOW}{self._identity.identity_id}{Style.RESET_ALL}", ] report_title = ( @@ -298,14 +354,13 @@ def print_credentials(self): # TODO: setup_session or setup_credentials? # This should be setup_credentials, since it is setting up the credentials for the provider + @staticmethod def setup_session( - self, m365_cli_auth: bool, - m365_app_auth: bool, + m365_env_app_auth: bool, m365_browser_auth: bool, tenant_id: str, - client_id: str, - client_secret: str, + microsoft365_credentials: dict, region_config: Microsoft365RegionConfig, ): """Returns the Microsoft365 credentials object. @@ -314,7 +369,7 @@ def setup_session( Args: m365_cli_auth (bool): Flag indicating whether to use Azure CLI authentication. - m365_app_auth (bool): Flag indicating whether to use application authentication with environment variables. + m365_env_app_auth (bool): Flag indicating whether to use application authentication with environment variables. m365_browser_auth (bool): Flag indicating whether to use interactive browser authentication. tenant_id (str): The Microsoft365 Active Directory tenant ID. m365_credentials (dict): The Microsoft365 configuration object. It contains the following keys: @@ -330,44 +385,53 @@ def setup_session( Exception: If failed to retrieve Microsoft365 credentials. """ - microsoft365_credentials = None - - try: - # Try to get credentials from cli arguments - if m365_app_auth and tenant_id and client_id and client_secret: - self.validate_arguments(tenant_id, client_id, client_secret) - self._auth_method = "Client Secret Credentials" - microsoft365_credentials = { - "tenant_id": tenant_id, - "client_id": client_id, - "client_secret": client_secret, - } - elif m365_cli_auth and tenant_id: - self._auth_method = "CLI Authentication" - microsoft365_credentials = { - "tenant_id": tenant_id, - } - # If not credentials are provided, try to get them from environment variables - else: - microsoft365_credentials = ClientSecretCredential( - tenant_id=getenv("APP_TENANT_ID", ""), - client_id=getenv("APP_CLIENT_ID", ""), - client_secret=getenv("APP_CLIENT_SECRET", ""), - ) - if not m365_browser_auth and (m365_app_auth or m365_cli_auth): - # Since the authentication method to be used will come as True, we have to negate it since - # DefaultAzureCredential sets just one authentication method, excluding the others + # Browser auth creds cannot be set with DefaultAzureCredentials() + if not m365_browser_auth: + try: + if ( + m365_env_app_auth + and Microsoft365Provider.check_application_creds_env_vars() + ): try: - microsoft365_credentials = DefaultAzureCredential( - exclude_environment_credential=True, + credentials = ClientSecretCredential( + tenant_id=getenv("APP_TENANT_ID"), + client_id=getenv("APP_CLIENT_ID"), + client_secret=getenv("APP_CLIENT_SECRET"), + ) + return credentials + except ClientAuthenticationError as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365ClientAuthenticationError( + file=os.path.basename(__file__), original_exception=error + ) + except CredentialUnavailableError as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365CredentialsUnavailableError( + file=os.path.basename(__file__), original_exception=error + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365ConfigCredentialsError( + file=os.path.basename(__file__), original_exception=error + ) + else: + try: + credentials = DefaultAzureCredential( + exclude_environment_credential=not m365_env_app_auth, exclude_cli_credential=not m365_cli_auth, - # M365 does not support managed identity authentication + # Microsoft365 Auth using Managed Identity is not supported exclude_managed_identity_credential=True, - # Azure Auth using Visual Studio is not supported + # Microsoft365 Auth using Visual Studio is not supported exclude_visual_studio_code_credential=True, - # Azure Auth using Shared Token Cache is not supported + # Microsoft365 Auth using Shared Token Cache is not supported exclude_shared_token_cache_credential=True, - # Azure Auth using PowerShell is not supported + # Microsoft365 Auth using PowerShell is not supported exclude_powershell_credential=True, # set Authority of a Microsoft Entra endpoint authority=region_config.authority, @@ -379,44 +443,43 @@ def setup_session( raise Microsoft365ClientAuthenticationError( file=os.path.basename(__file__), original_exception=error ) - except Exception as error: + except CredentialUnavailableError as error: logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" ) - raise Microsoft365ConfigCredentialsError( + raise Microsoft365CredentialsUnavailableError( file=os.path.basename(__file__), original_exception=error ) - # Browser auth creds cannot be set with DefaultAzureCredentials() - elif m365_browser_auth: - try: - microsoft365_credentials = InteractiveBrowserCredential( - tenant_id=tenant_id - ) - self._auth_method = "Browser Authentication" except Exception as error: - logger.critical( - "Failed to retrieve azure credentials using browser authentication" - ) - logger.critical( + logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" ) - raise Microsoft365InteractiveBrowserCredentialError( + raise Microsoft365DefaultAzureCredentialError( file=os.path.basename(__file__), original_exception=error ) - if not microsoft365_credentials: - raise Microsoft365EnvironmentVariableError( - file=os.path.basename(__file__), - message="No authentication method selected and no environment variables were found.", - ) - except Exception as error: - logger.critical("Failed to retrieve Microsoft365 credentials") - logger.critical( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - raise Microsoft365SetUpSessionError( - file=os.path.basename(__file__), original_exception=error - ) - return microsoft365_credentials + except Exception as error: + logger.critical("Failed to retrieve Microsoft365 credentials") + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365SetUpSessionError( + file=os.path.basename(__file__), original_exception=error + ) + else: + try: + credentials = InteractiveBrowserCredential(tenant_id=tenant_id) + except Exception as error: + logger.critical( + "Failed to retrieve Microsoft365 credentials using browser authentication" + ) + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365InteractiveBrowserCredentialError( + file=os.path.basename(__file__), original_exception=error + ) + + return credentials @staticmethod def test_connection( @@ -593,32 +656,36 @@ def check_application_creds_env_vars(): - APP_TENANT_ID: Microsoft365 tenant ID - APP_CLIENT_SECRET: Microsoft365 client secret - If any of the environment variables is missing, it logs a critical error and exits the program. + Returns: + bool: True if all environment variables are present, False otherwise. """ logger.info( "Microsoft365 provider: checking service principal environment variables ..." ) - all_present = True + env_vars_missing = False for env_var in ["APP_CLIENT_ID", "APP_TENANT_ID", "APP_CLIENT_SECRET"]: if not getenv(env_var): logger.critical( f"Microsoft365 provider: Missing environment variable {env_var} needed to authenticate against Microsoft365" ) - all_present = False - raise Microsoft365EnvironmentVariableError( - file=os.path.basename(__file__), - message=f"Missing environment variable {env_var} required to authenticate.", - ) - return all_present + env_vars_missing = True + return not env_vars_missing def setup_identity( self, + m365_cli_auth, + m365_env_app_auth, + m365_browser_auth, + client_id, ): """ Sets up the identity for the Microsoft365 provider. Args: - None + m365_cli_auth (bool): Flag indicating if Azure CLI authentication is used. + m365_env_app_auth (bool): Flag indicating if application authentication with environment variables is used. + m365_browser_auth (bool): Flag indicating if interactive browser authentication is used. + client_id (str): The Microsoft365 client ID. Returns: Microsoft365IdentityInfo: An instance of Microsoft365IdentityInfo containing the identity information. @@ -631,49 +698,70 @@ def setup_identity( # the identity can access AAD and retrieve the tenant domain name. # With cli also should be possible but right now it does not work, microsoft365 python package issue is coming # At the time of writting this with az cli creds is not working, despite that is included + if m365_cli_auth or m365_env_app_auth or m365_browser_auth or client_id: - async def get_microsoft365_identity(): - # Trying to recover tenant domain info - try: - logger.info( - "Trying to retrieve tenant domain from AAD to populate identity structure ..." - ) - client = GraphServiceClient(credentials=credentials) + async def get_microsoft365_identity(): + # Trying to recover tenant domain info + try: + logger.info( + "Trying to retrieve tenant domain from AAD to populate identity structure ..." + ) + client = GraphServiceClient(credentials=credentials) - domain_result = await client.domains.get() - if getattr(domain_result, "value"): - if getattr(domain_result.value[0], "id"): - identity.tenant_domain = domain_result.value[0].id + domain_result = await client.domains.get() + if getattr(domain_result, "value"): + if getattr(domain_result.value[0], "id"): + identity.tenant_domain = domain_result.value[0].id - except HttpResponseError as error: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - raise Microsoft365HTTPResponseError( - file=os.path.basename(__file__), - original_exception=error, - ) - except ClientAuthenticationError as error: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - raise Microsoft365GetTokenIdentityError( - file=os.path.basename(__file__), - original_exception=error, - ) - except Exception as error: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - # since that exception is not considered as critical, we keep filling another identity fields - # The id of the sp can be retrieved from environment variables - identity.identity_id = getenv("APP_CLIENT_ID") - identity.identity_type = "Application" - identity.tenant_id = getenv("APP_TENANT_ID") + except HttpResponseError as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365HTTPResponseError( + file=os.path.basename(__file__), + original_exception=error, + ) + except ClientAuthenticationError as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise Microsoft365GetTokenIdentityError( + file=os.path.basename(__file__), + original_exception=error, + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + # since that exception is not considered as critical, we keep filling another identity fields + # The id of the sp can be retrieved from environment variables + if m365_env_app_auth or client_id: + identity.identity_id = getenv("APP_CLIENT_ID") + identity.identity_type = "Application" + identity.tenant_id = getenv("APP_TENANT_ID") + # Same here, if user can access AAD, some fields are retrieved if not, default value, for az cli + # should work but it doesn't, pending issue + else: + identity.identity_id = "Unknown user id (Missing AAD permissions)" + identity.identity_type = "User" + try: + logger.info( + "Trying to retrieve user information from AAD to populate identity structure ..." + ) + client = GraphServiceClient(credentials=credentials) + + me = await client.me.get() + if me: + if getattr(me, "user_principal_name"): + identity.identity_id = me.user_principal_name - asyncio.get_event_loop().run_until_complete(get_microsoft365_identity()) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) - return identity + asyncio.get_event_loop().run_until_complete(get_microsoft365_identity()) + return identity @staticmethod def validate_static_credentials( From a7dff9c6ae7524732664a5b688c1ffdc0036f4b1 Mon Sep 17 00:00:00 2001 From: HugoPBrito Date: Thu, 23 Jan 2025 10:15:37 +0100 Subject: [PATCH 30/44] feat: improve error logs and add auth tests --- .../microsoft365/microsoft365_provider.py | 20 +-- .../microsoft365/microsoft365_fixtures.py | 2 + .../microsoft365_provider_test.py | 130 +++++++++++++++++- 3 files changed, 137 insertions(+), 15 deletions(-) diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index f09e6267258..c5d153d13c7 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -420,10 +420,10 @@ def setup_session( raise Microsoft365ConfigCredentialsError( file=os.path.basename(__file__), original_exception=error ) - else: + elif m365_cli_auth: try: credentials = DefaultAzureCredential( - exclude_environment_credential=not m365_env_app_auth, + exclude_environment_credential=True, exclude_cli_credential=not m365_cli_auth, # Microsoft365 Auth using Managed Identity is not supported exclude_managed_identity_credential=True, @@ -662,14 +662,18 @@ def check_application_creds_env_vars(): logger.info( "Microsoft365 provider: checking service principal environment variables ..." ) - env_vars_missing = False + missing_env_vars = [] for env_var in ["APP_CLIENT_ID", "APP_TENANT_ID", "APP_CLIENT_SECRET"]: if not getenv(env_var): - logger.critical( - f"Microsoft365 provider: Missing environment variable {env_var} needed to authenticate against Microsoft365" - ) - env_vars_missing = True - return not env_vars_missing + missing_env_vars.append(env_var) + + if missing_env_vars: + raise Microsoft365CredentialsUnavailableError( + file=os.path.basename(__file__), + message=f"Missing environment variables needed to authenticate against Microsoft365: {', '.join(missing_env_vars)}", + ) + else: + return True def setup_identity( self, diff --git a/tests/providers/microsoft365/microsoft365_fixtures.py b/tests/providers/microsoft365/microsoft365_fixtures.py index a27c43e6fac..79e8ec118f1 100644 --- a/tests/providers/microsoft365/microsoft365_fixtures.py +++ b/tests/providers/microsoft365/microsoft365_fixtures.py @@ -11,6 +11,8 @@ IDENTITY_ID = "00000000-0000-0000-0000-000000000000" IDENTITY_TYPE = "Application" TENANT_ID = "00000000-0000-0000-0000-000000000000" +CLIENT_ID = "00000000-0000-0000-0000-000000000000" +CLIENT_SECRET = "00000000-0000-0000-0000-000000000000" DOMAIN = "user.onmicrosoft.com" LOCATION = "global" diff --git a/tests/providers/microsoft365/microsoft365_provider_test.py b/tests/providers/microsoft365/microsoft365_provider_test.py index d15cd4198e7..7118b067fb7 100644 --- a/tests/providers/microsoft365/microsoft365_provider_test.py +++ b/tests/providers/microsoft365/microsoft365_provider_test.py @@ -2,7 +2,11 @@ from uuid import uuid4 import pytest -from azure.identity import ClientSecretCredential +from azure.identity import ( + ClientSecretCredential, + DefaultAzureCredential, + InteractiveBrowserCredential, +) from mock import MagicMock from prowler.config.config import ( @@ -17,6 +21,8 @@ Microsoft365RegionConfig, ) from tests.providers.microsoft365.microsoft365_fixtures import ( + CLIENT_ID, + CLIENT_SECRET, DOMAIN, IDENTITY_ID, IDENTITY_TYPE, @@ -40,9 +46,9 @@ def test_microsoft365_provider(self): patch( "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session", return_value=ClientSecretCredential( - client_id=IDENTITY_ID, + client_id=CLIENT_ID, tenant_id=TENANT_ID, - client_secret="client_secret", + client_secret=CLIENT_SECRET, ), ), patch( @@ -57,12 +63,15 @@ def test_microsoft365_provider(self): ), ): microsoft365_provider = Microsoft365Provider( - tenant_id, - azure_region, - config_path=default_config_file_path, - fixer_config=fixer_config, + m365_env_app_auth=True, + m365_cli_auth=False, + m365_browser_auth=False, + tenant_id=tenant_id, client_id=client_id, client_secret=client_secret, + region=azure_region, + config_path=default_config_file_path, + fixer_config=fixer_config, ) assert microsoft365_provider.region_config == Microsoft365RegionConfig( @@ -119,3 +128,110 @@ def test_test_connection_with_exception(self): assert exception.type is Exception assert exception.value.args[0] == "Simulated Exception" + + def test_microsoft365_provider_cli_auth(self): + """Test Microsoft365 Provider initialization with CLI authentication""" + azure_region = "Microsoft365Global" + fixer_config = load_and_validate_config_file( + "microsoft365", default_fixer_config_file_path + ) + + with ( + patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session", + return_value=DefaultAzureCredential( + exclude_environment_credential=True, + exclude_cli_credential=False, + exclude_managed_identity_credential=True, + exclude_visual_studio_code_credential=True, + exclude_shared_token_cache_credential=True, + exclude_powershell_credential=True, + exclude_browser_credential=True, + ), + ), + patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_identity", + return_value=Microsoft365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type="User", + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + location=LOCATION, + ), + ), + ): + microsoft365_provider = Microsoft365Provider( + m365_env_app_auth=False, + m365_cli_auth=True, + m365_browser_auth=False, + region=azure_region, + config_path=default_config_file_path, + fixer_config=fixer_config, + ) + + assert microsoft365_provider.region_config == Microsoft365RegionConfig( + name="Microsoft365Global", + authority=None, + base_url="https://graph.microsoft.com", + credential_scopes=["https://graph.microsoft.com/.default"], + ) + assert microsoft365_provider.identity == Microsoft365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type="User", + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + location=LOCATION, + ) + assert isinstance(microsoft365_provider.session, DefaultAzureCredential) + + def test_microsoft365_provider_browser_auth(self): + """Test Microsoft365 Provider initialization with Browser authentication""" + azure_region = "Microsoft365Global" + fixer_config = load_and_validate_config_file( + "microsoft365", default_fixer_config_file_path + ) + + with ( + patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session", + return_value=InteractiveBrowserCredential( + tenant_id=TENANT_ID, + ), + ), + patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_identity", + return_value=Microsoft365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type="User", + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + location=LOCATION, + ), + ), + ): + microsoft365_provider = Microsoft365Provider( + m365_env_app_auth=False, + m365_cli_auth=False, + m365_browser_auth=True, + tenant_id=TENANT_ID, + region=azure_region, + config_path=default_config_file_path, + fixer_config=fixer_config, + ) + + assert microsoft365_provider.region_config == Microsoft365RegionConfig( + name="Microsoft365Global", + authority=None, + base_url="https://graph.microsoft.com", + credential_scopes=["https://graph.microsoft.com/.default"], + ) + assert microsoft365_provider.identity == Microsoft365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type="User", + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + location=LOCATION, + ) + assert isinstance( + microsoft365_provider.session, InteractiveBrowserCredential + ) From 325b26ca7f96a96e6807d60fb4bdf6f2411e1dac Mon Sep 17 00:00:00 2001 From: MrCloudSec Date: Thu, 23 Jan 2025 10:47:49 -0500 Subject: [PATCH 31/44] chore: change args names --- .../microsoft365/lib/arguments/arguments.py | 6 +- .../microsoft365/microsoft365_provider.py | 80 +++++++++---------- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/prowler/providers/microsoft365/lib/arguments/arguments.py b/prowler/providers/microsoft365/lib/arguments/arguments.py index 0a8112ec4c3..02aaccd4a6a 100644 --- a/prowler/providers/microsoft365/lib/arguments/arguments.py +++ b/prowler/providers/microsoft365/lib/arguments/arguments.py @@ -16,17 +16,17 @@ def init_parser(self): microsoft365_auth_subparser.add_mutually_exclusive_group() ) microsoft365_auth_modes_group.add_argument( - "--m365-cli-auth", + "--cli-auth", action="store_true", help="Use Azure CLI authentication to log in against Microsoft365", ) microsoft365_auth_modes_group.add_argument( - "--m365-env-app-auth", + "--env-app-auth", action="store_true", help="Use application authentication with environment variables to log in against Microsoft365", ) microsoft365_auth_modes_group.add_argument( - "--m365-browser-auth", + "--browser-auth", action="store_true", help="Use interactive browser authentication to log in against Microsoft365", ) diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index c5d153d13c7..c4a9a4785bf 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -105,9 +105,9 @@ class Microsoft365Provider(Provider): def __init__( self, - m365_env_app_auth: bool, - m365_cli_auth: bool, - m365_browser_auth: bool, + env_app_auth: bool, + cli_auth: bool, + browser_auth: bool, tenant_id: str = None, client_id: str = None, client_secret: str = None, @@ -148,9 +148,9 @@ def __init__( # Validate the authentication arguments self.validate_arguments( - m365_cli_auth, - m365_env_app_auth, - m365_browser_auth, + cli_auth, + env_app_auth, + browser_auth, tenant_id, client_id, client_secret, @@ -168,9 +168,9 @@ def __init__( # Set up the Microsoft365 session self._session = self.setup_session( - m365_cli_auth, - m365_env_app_auth, - m365_browser_auth, + cli_auth, + env_app_auth, + browser_auth, tenant_id, microsoft365_credentials, self._region_config, @@ -178,9 +178,9 @@ def __init__( # Set up the identity self._identity = self.setup_identity( - m365_cli_auth, - m365_env_app_auth, - m365_browser_auth, + cli_auth, + env_app_auth, + browser_auth, client_id, ) @@ -246,9 +246,9 @@ def mutelist(self) -> Microsoft365Mutelist: @staticmethod def validate_arguments( - m365_cli_auth: bool, - m365_env_app_auth: bool, - m365_browser_auth: bool, + cli_auth: bool, + env_app_auth: bool, + browser_auth: bool, tenant_id: str, client_id: str, client_secret: str, @@ -257,9 +257,9 @@ def validate_arguments( Validates the authentication arguments for the Microsoft365 provider. Args: - m365_cli_auth (bool): Flag indicating whether Azure CLI authentication is enabled. - m365_env_app_auth (bool): Flag indicating whether application authentication with environment variables is enabled. - m365_browser_auth (bool): Flag indicating whether browser authentication is enabled. + cli_auth (bool): Flag indicating whether Azure CLI authentication is enabled. + env_app_auth (bool): Flag indicating whether application authentication with environment variables is enabled. + browser_auth (bool): Flag indicating whether browser authentication is enabled. tenant_id (str): The Microsoft365 Tenant ID. client_id (str): The Microsoft365 Client ID. client_secret (str): The Microsoft365 Client Secret. @@ -269,17 +269,17 @@ def validate_arguments( """ if not client_id and not client_secret: - if not m365_browser_auth and tenant_id: + if not browser_auth and tenant_id: raise Microsoft365BrowserAuthNoFlagError( file=os.path.basename(__file__), message="Microsoft365 Tenant ID (--m365-browser-auth) is required for browser authentication mode", ) - elif not m365_cli_auth and not m365_env_app_auth and not m365_browser_auth: + elif not cli_auth and not env_app_auth and not browser_auth: raise Microsoft365NoAuthenticationMethodError( file=os.path.basename(__file__), message="Microsoft365 provider requires at least one authentication method set: [--m365-cli-auth | --m365-env-app-auth | --m365-browser-auth]", ) - elif m365_browser_auth and not tenant_id: + elif browser_auth and not tenant_id: raise Microsoft365BrowserAuthNoTenantIDError( file=os.path.basename(__file__), message="Microsoft365 Tenant ID (--tenant-id) is required for browser authentication mode", @@ -356,9 +356,9 @@ def print_credentials(self): # This should be setup_credentials, since it is setting up the credentials for the provider @staticmethod def setup_session( - m365_cli_auth: bool, - m365_env_app_auth: bool, - m365_browser_auth: bool, + cli_auth: bool, + env_app_auth: bool, + browser_auth: bool, tenant_id: str, microsoft365_credentials: dict, region_config: Microsoft365RegionConfig, @@ -368,11 +368,11 @@ def setup_session( Set up the Microsoft365 session with the specified authentication method. Args: - m365_cli_auth (bool): Flag indicating whether to use Azure CLI authentication. - m365_env_app_auth (bool): Flag indicating whether to use application authentication with environment variables. - m365_browser_auth (bool): Flag indicating whether to use interactive browser authentication. + cli_auth (bool): Flag indicating whether to use Azure CLI authentication. + env_app_auth (bool): Flag indicating whether to use application authentication with environment variables. + browser_auth (bool): Flag indicating whether to use interactive browser authentication. tenant_id (str): The Microsoft365 Active Directory tenant ID. - m365_credentials (dict): The Microsoft365 configuration object. It contains the following keys: + credentials (dict): The Microsoft365 configuration object. It contains the following keys: - tenant_id: The Microsoft365 Active Directory tenant ID. - client_id: The Microsoft365 client ID. - client_secret: The Microsoft365 client secret @@ -386,10 +386,10 @@ def setup_session( """ # Browser auth creds cannot be set with DefaultAzureCredentials() - if not m365_browser_auth: + if not browser_auth: try: if ( - m365_env_app_auth + env_app_auth and Microsoft365Provider.check_application_creds_env_vars() ): try: @@ -420,11 +420,11 @@ def setup_session( raise Microsoft365ConfigCredentialsError( file=os.path.basename(__file__), original_exception=error ) - elif m365_cli_auth: + elif cli_auth: try: credentials = DefaultAzureCredential( exclude_environment_credential=True, - exclude_cli_credential=not m365_cli_auth, + exclude_cli_credential=not cli_auth, # Microsoft365 Auth using Managed Identity is not supported exclude_managed_identity_credential=True, # Microsoft365 Auth using Visual Studio is not supported @@ -677,18 +677,18 @@ def check_application_creds_env_vars(): def setup_identity( self, - m365_cli_auth, - m365_env_app_auth, - m365_browser_auth, + cli_auth, + env_app_auth, + browser_auth, client_id, ): """ Sets up the identity for the Microsoft365 provider. Args: - m365_cli_auth (bool): Flag indicating if Azure CLI authentication is used. - m365_env_app_auth (bool): Flag indicating if application authentication with environment variables is used. - m365_browser_auth (bool): Flag indicating if interactive browser authentication is used. + cli_auth (bool): Flag indicating if Azure CLI authentication is used. + env_app_auth (bool): Flag indicating if application authentication with environment variables is used. + browser_auth (bool): Flag indicating if interactive browser authentication is used. client_id (str): The Microsoft365 client ID. Returns: @@ -702,7 +702,7 @@ def setup_identity( # the identity can access AAD and retrieve the tenant domain name. # With cli also should be possible but right now it does not work, microsoft365 python package issue is coming # At the time of writting this with az cli creds is not working, despite that is included - if m365_cli_auth or m365_env_app_auth or m365_browser_auth or client_id: + if cli_auth or env_app_auth or browser_auth or client_id: async def get_microsoft365_identity(): # Trying to recover tenant domain info @@ -739,7 +739,7 @@ async def get_microsoft365_identity(): ) # since that exception is not considered as critical, we keep filling another identity fields # The id of the sp can be retrieved from environment variables - if m365_env_app_auth or client_id: + if env_app_auth or client_id: identity.identity_id = getenv("APP_CLIENT_ID") identity.identity_type = "Application" identity.tenant_id = getenv("APP_TENANT_ID") From c926937342c27f4813eabeac86cdafa0e70a71a0 Mon Sep 17 00:00:00 2001 From: MrCloudSec Date: Thu, 23 Jan 2025 11:36:44 -0500 Subject: [PATCH 32/44] chore: revision --- prowler/lib/check/models.py | 4 --- prowler/lib/outputs/html/html.py | 8 ++--- prowler/providers/common/provider.py | 6 ++-- .../microsoft365/lib/arguments/arguments.py | 2 +- .../microsoft365/lib/service/service.py | 3 -- .../microsoft365/microsoft365_provider.py | 32 +++++++++--------- ...dmincenter_groups_not_public_visibility.py | 2 -- ..._users_admins_reduced_license_footprint.py | 2 -- ...sers_between_two_and_four_global_admins.py | 2 -- .../microsoft365_provider_test.py | 33 ++++++++++--------- 10 files changed, 41 insertions(+), 53 deletions(-) diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index c2346ec80cc..07fd434a766 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -490,16 +490,12 @@ class Check_Report_Microsoft365(Check_Report): resource_name: str resource_id: str - tenant_id: str - tenant_domain: str location: str def __init__(self, metadata): super().__init__(metadata) self.resource_name = "" self.resource_id = "" - self.tenant_id = "" - self.tenant_domain = "" self.location = "global" diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py index 6d2f49c603c..f1944b6ee63 100644 --- a/prowler/lib/outputs/html/html.py +++ b/prowler/lib/outputs/html/html.py @@ -554,7 +554,7 @@ def get_microsoft365_assessment_summary(provider: Provider) -> str:
- Azure Assessment Summary + Microsoft365 Assessment Summary
  • @@ -566,14 +566,14 @@ def get_microsoft365_assessment_summary(provider: Provider) -> str:
    - Azure Credentials + Microsoft365 Credentials
    • - Azure Identity Type: {provider.identity.identity_type} + Microsoft365 Identity Type: {provider.identity.identity_type}
    • - Azure Identity ID: {provider.identity.identity_id} + Microsoft365 Identity ID: {provider.identity.identity_id}
    diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index 01d1bfbf793..40045844939 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -217,9 +217,9 @@ def init_global_provider(arguments: Namespace) -> None: config_path=arguments.config_file, mutelist_path=arguments.mutelist_file, fixer_config=fixer_config, - m365_env_app_auth=arguments.m365_env_app_auth, - m365_cli_auth=arguments.m365_cli_auth, - m365_browser_auth=arguments.m365_browser_auth, + env_app_auth=arguments.env_app_auth, + az_cli_auth=arguments.az_cli_auth, + browser_auth=arguments.browser_auth, tenant_id=arguments.tenant_id, ) diff --git a/prowler/providers/microsoft365/lib/arguments/arguments.py b/prowler/providers/microsoft365/lib/arguments/arguments.py index 02aaccd4a6a..3f6ff9845a0 100644 --- a/prowler/providers/microsoft365/lib/arguments/arguments.py +++ b/prowler/providers/microsoft365/lib/arguments/arguments.py @@ -16,7 +16,7 @@ def init_parser(self): microsoft365_auth_subparser.add_mutually_exclusive_group() ) microsoft365_auth_modes_group.add_argument( - "--cli-auth", + "--az-cli-auth", action="store_true", help="Use Azure CLI authentication to log in against Microsoft365", ) diff --git a/prowler/providers/microsoft365/lib/service/service.py b/prowler/providers/microsoft365/lib/service/service.py index 3fe72937cd0..85f54b260eb 100644 --- a/prowler/providers/microsoft365/lib/service/service.py +++ b/prowler/providers/microsoft365/lib/service/service.py @@ -10,8 +10,5 @@ def __init__( ): self.client = GraphServiceClient(credentials=provider.session) - # self.locations = provider.locations - self.audited_tenant = provider.identity.tenant_id - self.audited_domain = provider.identity.tenant_domain self.audit_config = provider.audit_config self.fixer_config = provider.fixer_config diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index c4a9a4785bf..287276d5183 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -106,7 +106,7 @@ class Microsoft365Provider(Provider): def __init__( self, env_app_auth: bool, - cli_auth: bool, + az_cli_auth: bool, browser_auth: bool, tenant_id: str = None, client_id: str = None, @@ -148,7 +148,7 @@ def __init__( # Validate the authentication arguments self.validate_arguments( - cli_auth, + az_cli_auth, env_app_auth, browser_auth, tenant_id, @@ -168,7 +168,7 @@ def __init__( # Set up the Microsoft365 session self._session = self.setup_session( - cli_auth, + az_cli_auth, env_app_auth, browser_auth, tenant_id, @@ -178,7 +178,7 @@ def __init__( # Set up the identity self._identity = self.setup_identity( - cli_auth, + az_cli_auth, env_app_auth, browser_auth, client_id, @@ -246,7 +246,7 @@ def mutelist(self) -> Microsoft365Mutelist: @staticmethod def validate_arguments( - cli_auth: bool, + az_cli_auth: bool, env_app_auth: bool, browser_auth: bool, tenant_id: str, @@ -257,7 +257,7 @@ def validate_arguments( Validates the authentication arguments for the Microsoft365 provider. Args: - cli_auth (bool): Flag indicating whether Azure CLI authentication is enabled. + az_cli_auth (bool): Flag indicating whether Azure CLI authentication is enabled. env_app_auth (bool): Flag indicating whether application authentication with environment variables is enabled. browser_auth (bool): Flag indicating whether browser authentication is enabled. tenant_id (str): The Microsoft365 Tenant ID. @@ -272,12 +272,12 @@ def validate_arguments( if not browser_auth and tenant_id: raise Microsoft365BrowserAuthNoFlagError( file=os.path.basename(__file__), - message="Microsoft365 Tenant ID (--m365-browser-auth) is required for browser authentication mode", + message="Microsoft365 Tenant ID (--browser-auth) is required for browser authentication mode", ) - elif not cli_auth and not env_app_auth and not browser_auth: + elif not az_cli_auth and not env_app_auth and not browser_auth: raise Microsoft365NoAuthenticationMethodError( file=os.path.basename(__file__), - message="Microsoft365 provider requires at least one authentication method set: [--m365-cli-auth | --m365-env-app-auth | --m365-browser-auth]", + message="Microsoft365 provider requires at least one authentication method set: [--az-cli-auth | --env-app-auth | --browser-auth]", ) elif browser_auth and not tenant_id: raise Microsoft365BrowserAuthNoTenantIDError( @@ -356,7 +356,7 @@ def print_credentials(self): # This should be setup_credentials, since it is setting up the credentials for the provider @staticmethod def setup_session( - cli_auth: bool, + az_cli_auth: bool, env_app_auth: bool, browser_auth: bool, tenant_id: str, @@ -368,7 +368,7 @@ def setup_session( Set up the Microsoft365 session with the specified authentication method. Args: - cli_auth (bool): Flag indicating whether to use Azure CLI authentication. + az_cli_auth (bool): Flag indicating whether to use Azure CLI authentication. env_app_auth (bool): Flag indicating whether to use application authentication with environment variables. browser_auth (bool): Flag indicating whether to use interactive browser authentication. tenant_id (str): The Microsoft365 Active Directory tenant ID. @@ -420,11 +420,11 @@ def setup_session( raise Microsoft365ConfigCredentialsError( file=os.path.basename(__file__), original_exception=error ) - elif cli_auth: + elif az_cli_auth: try: credentials = DefaultAzureCredential( exclude_environment_credential=True, - exclude_cli_credential=not cli_auth, + exclude_cli_credential=not az_cli_auth, # Microsoft365 Auth using Managed Identity is not supported exclude_managed_identity_credential=True, # Microsoft365 Auth using Visual Studio is not supported @@ -677,7 +677,7 @@ def check_application_creds_env_vars(): def setup_identity( self, - cli_auth, + az_cli_auth, env_app_auth, browser_auth, client_id, @@ -686,7 +686,7 @@ def setup_identity( Sets up the identity for the Microsoft365 provider. Args: - cli_auth (bool): Flag indicating if Azure CLI authentication is used. + az_cli_auth (bool): Flag indicating if Azure CLI authentication is used. env_app_auth (bool): Flag indicating if application authentication with environment variables is used. browser_auth (bool): Flag indicating if interactive browser authentication is used. client_id (str): The Microsoft365 client ID. @@ -702,7 +702,7 @@ def setup_identity( # the identity can access AAD and retrieve the tenant domain name. # With cli also should be possible but right now it does not work, microsoft365 python package issue is coming # At the time of writting this with az cli creds is not working, despite that is included - if cli_auth or env_app_auth or browser_auth or client_id: + if az_cli_auth or env_app_auth or browser_auth or client_id: async def get_microsoft365_identity(): # Trying to recover tenant domain info diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py index d353f4ac8c6..24543379e1a 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py @@ -11,8 +11,6 @@ def execute(self) -> Check_Report_Microsoft365: report = Check_Report_Microsoft365(self.metadata()) report.resource_id = group.id report.resource_name = group.name - report.tenant_id = admincenter_client.audited_tenant - report.tenant_domain = admincenter_client.audited_domain report.status = "FAIL" report.status_extended = f"Group {group.name} has {group.visibility} visibility and should be Private." diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py index 4302549dbe7..8ffe1067243 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py @@ -21,8 +21,6 @@ def execute(self) -> Check_Report_Microsoft365: report = Check_Report_Microsoft365(self.metadata()) report.resource_id = user.id report.resource_name = user.name - report.tenant_id = admincenter_client.audited_tenant - report.tenant_domain = admincenter_client.audited_domain report.status = "FAIL" report.status_extended = f"User {user.name} has administrative roles {admin_roles} and an invalid license {user.license if user.license else ''}." diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py index 8feda8cf17a..c22d022d4a4 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py @@ -11,8 +11,6 @@ def execute(self) -> Check_Report_Microsoft365: directory_roles = admincenter_client.directory_roles report = Check_Report_Microsoft365(self.metadata()) report.status = "FAIL" - report.tenant_id = admincenter_client.audited_tenant - report.tenant_domain = admincenter_client.audited_domain report.resource_name = "Global Administrator" if "Global Administrator" in directory_roles: diff --git a/tests/providers/microsoft365/microsoft365_provider_test.py b/tests/providers/microsoft365/microsoft365_provider_test.py index 7118b067fb7..d6bc94d8f41 100644 --- a/tests/providers/microsoft365/microsoft365_provider_test.py +++ b/tests/providers/microsoft365/microsoft365_provider_test.py @@ -63,9 +63,9 @@ def test_microsoft365_provider(self): ), ): microsoft365_provider = Microsoft365Provider( - m365_env_app_auth=True, - m365_cli_auth=False, - m365_browser_auth=False, + env_app_auth=True, + az_cli_auth=False, + browser_auth=False, tenant_id=tenant_id, client_id=client_id, client_secret=client_secret, @@ -89,12 +89,14 @@ def test_microsoft365_provider(self): ) def test_test_connection_tenant_id_client_id_client_secret(self): - with patch( - "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" - ) as mock_setup_session, patch( - "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.validate_static_credentials" - ) as mock_validate_static_credentials: - + with ( + patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" + ) as mock_setup_session, + patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.validate_static_credentials" + ) as mock_validate_static_credentials, + ): # Mock setup_session to return a mocked session object mock_session = MagicMock() mock_setup_session.return_value = mock_session @@ -118,7 +120,6 @@ def test_test_connection_with_exception(self): with patch( "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" ) as mock_setup_session: - mock_setup_session.side_effect = Exception("Simulated Exception") with pytest.raises(Exception) as exception: @@ -161,9 +162,9 @@ def test_microsoft365_provider_cli_auth(self): ), ): microsoft365_provider = Microsoft365Provider( - m365_env_app_auth=False, - m365_cli_auth=True, - m365_browser_auth=False, + env_app_auth=False, + az_cli_auth=True, + browser_auth=False, region=azure_region, config_path=default_config_file_path, fixer_config=fixer_config, @@ -210,9 +211,9 @@ def test_microsoft365_provider_browser_auth(self): ), ): microsoft365_provider = Microsoft365Provider( - m365_env_app_auth=False, - m365_cli_auth=False, - m365_browser_auth=True, + env_app_auth=False, + az_cli_auth=False, + browser_auth=True, tenant_id=TENANT_ID, region=azure_region, config_path=default_config_file_path, From 4d4a19ac9ed04ca42a8bc662cb6c45789e95cd39 Mon Sep 17 00:00:00 2001 From: MrCloudSec Date: Thu, 23 Jan 2025 11:57:59 -0500 Subject: [PATCH 33/44] chore: revision --- prowler/providers/common/provider.py | 2 +- .../microsoft365/lib/arguments/arguments.py | 31 ++++------- .../microsoft365/microsoft365_provider.py | 52 +++++++++---------- .../microsoft365_provider_test.py | 6 +-- 4 files changed, 37 insertions(+), 54 deletions(-) diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index 40045844939..fd4015ada16 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -217,7 +217,7 @@ def init_global_provider(arguments: Namespace) -> None: config_path=arguments.config_file, mutelist_path=arguments.mutelist_file, fixer_config=fixer_config, - env_app_auth=arguments.env_app_auth, + sp_env_auth=arguments.sp_env_auth, az_cli_auth=arguments.az_cli_auth, browser_auth=arguments.browser_auth, tenant_id=arguments.tenant_id, diff --git a/prowler/providers/microsoft365/lib/arguments/arguments.py b/prowler/providers/microsoft365/lib/arguments/arguments.py index 3f6ff9845a0..d0a5dd771d0 100644 --- a/prowler/providers/microsoft365/lib/arguments/arguments.py +++ b/prowler/providers/microsoft365/lib/arguments/arguments.py @@ -1,6 +1,3 @@ -from argparse import ArgumentTypeError - - def init_parser(self): """Init the Microsoft365 Provider CLI parser""" microsoft365_parser = self.subparsers.add_parser( @@ -21,9 +18,9 @@ def init_parser(self): help="Use Azure CLI authentication to log in against Microsoft365", ) microsoft365_auth_modes_group.add_argument( - "--env-app-auth", + "--sp-env-auth", action="store_true", - help="Use application authentication with environment variables to log in against Microsoft365", + help="Use Service Principal environment variables authentication to log in against Microsoft365", ) microsoft365_auth_modes_group.add_argument( "--browser-auth", @@ -42,20 +39,10 @@ def init_parser(self): "--region", nargs="?", default="Microsoft365Global", - type=validate_microsoft365_region, - help="microsoft365 region from `az cloud list --output table`, by default Microsoft365Global", - ) - - -def validate_microsoft365_region(region): - """validate_microsoft365_region validates if the region passed as argument is valid""" - regions_allowed = [ - "Microsoft365GlobalChina", - "Microsoft365USGovernment", - "Microsoft365Global", - ] - if region not in regions_allowed: - raise ArgumentTypeError( - f"Region {region} not allowed, allowed regions are {' '.join(regions_allowed)}" - ) - return region + choices=[ + "Microsoft365Global", + "Microsoft365GlobalChina", + "Microsoft365USGovernment", + ], + help="Microsoft365 region to be used, default is Microsoft365Global", + ) diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index 287276d5183..cc573335020 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -49,9 +49,6 @@ Microsoft365TenantIdAndClientIdNotBelongingToClientSecretError, Microsoft365TenantIdAndClientSecretNotBelongingToClientIdError, ) -from prowler.providers.microsoft365.lib.arguments.arguments import ( - validate_microsoft365_region, -) from prowler.providers.microsoft365.lib.mutelist.mutelist import Microsoft365Mutelist from prowler.providers.microsoft365.lib.regions.microsoft365_regions import ( get_regions_config, @@ -105,7 +102,7 @@ class Microsoft365Provider(Provider): def __init__( self, - env_app_auth: bool, + sp_env_auth: bool, az_cli_auth: bool, browser_auth: bool, tenant_id: str = None, @@ -149,7 +146,7 @@ def __init__( # Validate the authentication arguments self.validate_arguments( az_cli_auth, - env_app_auth, + sp_env_auth, browser_auth, tenant_id, client_id, @@ -169,7 +166,7 @@ def __init__( # Set up the Microsoft365 session self._session = self.setup_session( az_cli_auth, - env_app_auth, + sp_env_auth, browser_auth, tenant_id, microsoft365_credentials, @@ -179,7 +176,7 @@ def __init__( # Set up the identity self._identity = self.setup_identity( az_cli_auth, - env_app_auth, + sp_env_auth, browser_auth, client_id, ) @@ -247,7 +244,7 @@ def mutelist(self) -> Microsoft365Mutelist: @staticmethod def validate_arguments( az_cli_auth: bool, - env_app_auth: bool, + sp_env_auth: bool, browser_auth: bool, tenant_id: str, client_id: str, @@ -258,7 +255,7 @@ def validate_arguments( Args: az_cli_auth (bool): Flag indicating whether Azure CLI authentication is enabled. - env_app_auth (bool): Flag indicating whether application authentication with environment variables is enabled. + sp_env_auth (bool): Flag indicating whether application authentication with environment variables is enabled. browser_auth (bool): Flag indicating whether browser authentication is enabled. tenant_id (str): The Microsoft365 Tenant ID. client_id (str): The Microsoft365 Client ID. @@ -274,10 +271,10 @@ def validate_arguments( file=os.path.basename(__file__), message="Microsoft365 Tenant ID (--browser-auth) is required for browser authentication mode", ) - elif not az_cli_auth and not env_app_auth and not browser_auth: + elif not az_cli_auth and not sp_env_auth and not browser_auth: raise Microsoft365NoAuthenticationMethodError( file=os.path.basename(__file__), - message="Microsoft365 provider requires at least one authentication method set: [--az-cli-auth | --env-app-auth | --browser-auth]", + message="Microsoft365 provider requires at least one authentication method set: [--az-cli-auth | --sp-env-auth | --browser-auth]", ) elif browser_auth and not tenant_id: raise Microsoft365BrowserAuthNoTenantIDError( @@ -304,7 +301,6 @@ def setup_region_config(region): """ try: - validate_microsoft365_region(region) config = get_regions_config(region) return Microsoft365RegionConfig( @@ -357,7 +353,7 @@ def print_credentials(self): @staticmethod def setup_session( az_cli_auth: bool, - env_app_auth: bool, + sp_env_auth: bool, browser_auth: bool, tenant_id: str, microsoft365_credentials: dict, @@ -369,7 +365,7 @@ def setup_session( Args: az_cli_auth (bool): Flag indicating whether to use Azure CLI authentication. - env_app_auth (bool): Flag indicating whether to use application authentication with environment variables. + sp_env_auth (bool): Flag indicating whether to use application authentication with environment variables. browser_auth (bool): Flag indicating whether to use interactive browser authentication. tenant_id (str): The Microsoft365 Active Directory tenant ID. credentials (dict): The Microsoft365 configuration object. It contains the following keys: @@ -389,14 +385,14 @@ def setup_session( if not browser_auth: try: if ( - env_app_auth + sp_env_auth and Microsoft365Provider.check_application_creds_env_vars() ): try: credentials = ClientSecretCredential( - tenant_id=getenv("APP_TENANT_ID"), - client_id=getenv("APP_CLIENT_ID"), - client_secret=getenv("APP_CLIENT_SECRET"), + tenant_id=getenv("M365_TENANT_ID"), + client_id=getenv("M365_CLIENT_ID"), + client_secret=getenv("M365_CLIENT_SECRET"), ) return credentials except ClientAuthenticationError as error: @@ -652,9 +648,9 @@ def check_application_creds_env_vars(): Checks the presence of required environment variables for application authentication against Microsoft365. This method checks for the presence of the following environment variables: - - APP_CLIENT_ID: Microsoft365 client ID - - APP_TENANT_ID: Microsoft365 tenant ID - - APP_CLIENT_SECRET: Microsoft365 client secret + - M365_CLIENT_ID: Microsoft365 client ID + - M365_TENANT_ID: Microsoft365 tenant ID + - M365_CLIENT_SECRET: Microsoft365 client secret Returns: bool: True if all environment variables are present, False otherwise. @@ -663,7 +659,7 @@ def check_application_creds_env_vars(): "Microsoft365 provider: checking service principal environment variables ..." ) missing_env_vars = [] - for env_var in ["APP_CLIENT_ID", "APP_TENANT_ID", "APP_CLIENT_SECRET"]: + for env_var in ["M365_CLIENT_ID", "M365_TENANT_ID", "M365_CLIENT_SECRET"]: if not getenv(env_var): missing_env_vars.append(env_var) @@ -678,7 +674,7 @@ def check_application_creds_env_vars(): def setup_identity( self, az_cli_auth, - env_app_auth, + sp_env_auth, browser_auth, client_id, ): @@ -687,7 +683,7 @@ def setup_identity( Args: az_cli_auth (bool): Flag indicating if Azure CLI authentication is used. - env_app_auth (bool): Flag indicating if application authentication with environment variables is used. + sp_env_auth (bool): Flag indicating if application authentication with environment variables is used. browser_auth (bool): Flag indicating if interactive browser authentication is used. client_id (str): The Microsoft365 client ID. @@ -702,7 +698,7 @@ def setup_identity( # the identity can access AAD and retrieve the tenant domain name. # With cli also should be possible but right now it does not work, microsoft365 python package issue is coming # At the time of writting this with az cli creds is not working, despite that is included - if az_cli_auth or env_app_auth or browser_auth or client_id: + if az_cli_auth or sp_env_auth or browser_auth or client_id: async def get_microsoft365_identity(): # Trying to recover tenant domain info @@ -739,10 +735,10 @@ async def get_microsoft365_identity(): ) # since that exception is not considered as critical, we keep filling another identity fields # The id of the sp can be retrieved from environment variables - if env_app_auth or client_id: - identity.identity_id = getenv("APP_CLIENT_ID") + if sp_env_auth or client_id: + identity.identity_id = getenv("M365_CLIENT_ID") identity.identity_type = "Application" - identity.tenant_id = getenv("APP_TENANT_ID") + identity.tenant_id = getenv("M365_TENANT_ID") # Same here, if user can access AAD, some fields are retrieved if not, default value, for az cli # should work but it doesn't, pending issue else: diff --git a/tests/providers/microsoft365/microsoft365_provider_test.py b/tests/providers/microsoft365/microsoft365_provider_test.py index d6bc94d8f41..70881ee7d42 100644 --- a/tests/providers/microsoft365/microsoft365_provider_test.py +++ b/tests/providers/microsoft365/microsoft365_provider_test.py @@ -63,7 +63,7 @@ def test_microsoft365_provider(self): ), ): microsoft365_provider = Microsoft365Provider( - env_app_auth=True, + sp_env_auth=True, az_cli_auth=False, browser_auth=False, tenant_id=tenant_id, @@ -162,7 +162,7 @@ def test_microsoft365_provider_cli_auth(self): ), ): microsoft365_provider = Microsoft365Provider( - env_app_auth=False, + sp_env_auth=False, az_cli_auth=True, browser_auth=False, region=azure_region, @@ -211,7 +211,7 @@ def test_microsoft365_provider_browser_auth(self): ), ): microsoft365_provider = Microsoft365Provider( - env_app_auth=False, + sp_env_auth=False, az_cli_auth=False, browser_auth=True, tenant_id=TENANT_ID, From 02a270c34d3e6bbbdef242cae83099688585bc8f Mon Sep 17 00:00:00 2001 From: MrCloudSec Date: Thu, 23 Jan 2025 12:06:13 -0500 Subject: [PATCH 34/44] chore: revision --- .../microsoft365/microsoft365_provider.py | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index cc573335020..b2b5c63a9f7 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -381,7 +381,6 @@ def setup_session( Exception: If failed to retrieve Microsoft365 credentials. """ - # Browser auth creds cannot be set with DefaultAzureCredentials() if not browser_auth: try: if ( @@ -479,8 +478,11 @@ def setup_session( @staticmethod def test_connection( - tenant_id=None, - region="Microsoft365Global", + az_cli_auth: bool = False, + sp_env_auth: bool = False, + browser_auth: bool = False, + tenant_id: str = None, + region: str = "Microsoft365Global", raise_on_exception=True, client_id=None, client_secret=None, @@ -518,6 +520,14 @@ def test_connection( True """ try: + Microsoft365Provider.validate_arguments( + az_cli_auth, + sp_env_auth, + browser_auth, + tenant_id, + client_id, + client_secret, + ) region_config = Microsoft365Provider.setup_region_config(region) # Get the dict from the static credentials @@ -533,6 +543,11 @@ def test_connection( # Set up the Microsoft365 session credentials = Microsoft365Provider.setup_session( + az_cli_auth, + sp_env_auth, + browser_auth, + tenant_id, + region_config, microsoft365_credentials, region_config, ) From 4aaf4596e8b14eca019d4037706a69060352ca2d Mon Sep 17 00:00:00 2001 From: MrCloudSec Date: Thu, 23 Jan 2025 12:07:39 -0500 Subject: [PATCH 35/44] chore: revision --- prowler/providers/microsoft365/microsoft365_provider.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index b2b5c63a9f7..da56ad7b1e8 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -368,7 +368,7 @@ def setup_session( sp_env_auth (bool): Flag indicating whether to use application authentication with environment variables. browser_auth (bool): Flag indicating whether to use interactive browser authentication. tenant_id (str): The Microsoft365 Active Directory tenant ID. - credentials (dict): The Microsoft365 configuration object. It contains the following keys: + microsoft365_credentials (dict): The Microsoft365 configuration object. It contains the following keys: - tenant_id: The Microsoft365 Active Directory tenant ID. - client_id: The Microsoft365 client ID. - client_secret: The Microsoft365 client secret @@ -493,6 +493,9 @@ def test_connection( Args: + az_cli_auth (bool): Flag indicating whether to use Azure CLI authentication. + sp_env_auth (bool): Flag indicating whether to use application authentication with environment variables. + browser_auth (bool): Flag indicating whether to use interactive browser authentication. tenant_id (str): The Microsoft365 Active Directory tenant ID. region (str): The Microsoft365 region. raise_on_exception (bool): Flag indicating whether to raise an exception if the connection fails. From 979ad414e0c0ca5f263c7a8da8e82a2dcd6c8051 Mon Sep 17 00:00:00 2001 From: HugoPBrito Date: Thu, 23 Jan 2025 19:28:52 +0100 Subject: [PATCH 36/44] feat: add tenant id to cli auth identity --- prowler/providers/microsoft365/microsoft365_provider.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index da56ad7b1e8..83aad891f47 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -756,7 +756,6 @@ async def get_microsoft365_identity(): if sp_env_auth or client_id: identity.identity_id = getenv("M365_CLIENT_ID") identity.identity_type = "Application" - identity.tenant_id = getenv("M365_TENANT_ID") # Same here, if user can access AAD, some fields are retrieved if not, default value, for az cli # should work but it doesn't, pending issue else: @@ -778,6 +777,11 @@ async def get_microsoft365_identity(): f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" ) + # Retrieve tenant id from the client + client = GraphServiceClient(credentials=credentials) + organization_info = await client.organization.get() + identity.tenant_id = organization_info.value[0].id + asyncio.get_event_loop().run_until_complete(get_microsoft365_identity()) return identity From 41e48def952c909cfe38067d62887f1a4dbef966 Mon Sep 17 00:00:00 2001 From: HugoPBrito Date: Thu, 23 Jan 2025 19:59:23 +0100 Subject: [PATCH 37/44] fix: test connection --- .../microsoft365/microsoft365_provider.py | 1 - .../microsoft365_provider_test.py | 160 +++++++++++++----- 2 files changed, 118 insertions(+), 43 deletions(-) diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index 83aad891f47..4c60cff228b 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -550,7 +550,6 @@ def test_connection( sp_env_auth, browser_auth, tenant_id, - region_config, microsoft365_credentials, region_config, ) diff --git a/tests/providers/microsoft365/microsoft365_provider_test.py b/tests/providers/microsoft365/microsoft365_provider_test.py index 70881ee7d42..b475a02ca66 100644 --- a/tests/providers/microsoft365/microsoft365_provider_test.py +++ b/tests/providers/microsoft365/microsoft365_provider_test.py @@ -2,6 +2,7 @@ from uuid import uuid4 import pytest +from azure.core.credentials import AccessToken from azure.identity import ( ClientSecretCredential, DefaultAzureCredential, @@ -15,6 +16,10 @@ load_and_validate_config_file, ) from prowler.providers.common.models import Connection +from prowler.providers.microsoft365.exceptions.exceptions import ( + Microsoft365HTTPResponseError, + Microsoft365NoAuthenticationMethodError, +) from prowler.providers.microsoft365.microsoft365_provider import Microsoft365Provider from prowler.providers.microsoft365.models import ( Microsoft365IdentityInfo, @@ -88,48 +93,6 @@ def test_microsoft365_provider(self): location=LOCATION, ) - def test_test_connection_tenant_id_client_id_client_secret(self): - with ( - patch( - "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" - ) as mock_setup_session, - patch( - "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.validate_static_credentials" - ) as mock_validate_static_credentials, - ): - # Mock setup_session to return a mocked session object - mock_session = MagicMock() - mock_setup_session.return_value = mock_session - - # Mock ValidateStaticCredentials to avoid real API calls - mock_validate_static_credentials.return_value = None - - test_connection = Microsoft365Provider.test_connection( - tenant_id=str(uuid4()), - region="Microsoft365Global", - raise_on_exception=False, - client_id=str(uuid4()), - client_secret=str(uuid4()), - ) - - assert isinstance(test_connection, Connection) - assert test_connection.is_connected - assert test_connection.error is None - - def test_test_connection_with_exception(self): - with patch( - "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" - ) as mock_setup_session: - mock_setup_session.side_effect = Exception("Simulated Exception") - - with pytest.raises(Exception) as exception: - Microsoft365Provider.test_connection( - raise_on_exception=True, - ) - - assert exception.type is Exception - assert exception.value.args[0] == "Simulated Exception" - def test_microsoft365_provider_cli_auth(self): """Test Microsoft365 Provider initialization with CLI authentication""" azure_region = "Microsoft365Global" @@ -236,3 +199,116 @@ def test_microsoft365_provider_browser_auth(self): assert isinstance( microsoft365_provider.session, InteractiveBrowserCredential ) + + def test_test_connection_browser_auth(self): + with ( + patch( + "prowler.providers.microsoft365.microsoft365_provider.DefaultAzureCredential" + ) as mock_default_credential, + patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" + ) as mock_setup_session, + patch( + "prowler.providers.microsoft365.microsoft365_provider.GraphServiceClient" + ) as mock_graph_client, + ): + + # Mock the return value of DefaultAzureCredential + mock_credentials = MagicMock() + mock_credentials.get_token.return_value = AccessToken( + token="fake_token", expires_on=9999999999 + ) + mock_default_credential.return_value = mock_credentials + + # Mock setup_session to return a mocked session object + mock_session = MagicMock() + mock_setup_session.return_value = mock_session + + # Mock GraphServiceClient to avoid real API calls + mock_client = MagicMock() + mock_graph_client.return_value = mock_client + + test_connection = Microsoft365Provider.test_connection( + browser_auth=True, + tenant_id=str(uuid4()), + region="Microsoft365Global", + raise_on_exception=False, + ) + + assert isinstance(test_connection, Connection) + assert test_connection.is_connected + assert test_connection.error is None + + def test_test_connection_tenant_id_client_id_client_secret(self): + with ( + patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" + ) as mock_setup_session, + patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.validate_static_credentials" + ) as mock_validate_static_credentials, + ): + # Mock setup_session to return a mocked session object + mock_session = MagicMock() + mock_setup_session.return_value = mock_session + + # Mock ValidateStaticCredentials to avoid real API calls + mock_validate_static_credentials.return_value = None + + test_connection = Microsoft365Provider.test_connection( + tenant_id=str(uuid4()), + region="Microsoft365Global", + raise_on_exception=False, + client_id=str(uuid4()), + client_secret=str(uuid4()), + ) + + assert isinstance(test_connection, Connection) + assert test_connection.is_connected + assert test_connection.error is None + + def test_test_connection_with_httpresponseerror(self): + with patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" + ) as mock_setup_session: + + mock_setup_session.side_effect = Microsoft365HTTPResponseError( + file="test_file", original_exception="Simulated HttpResponseError" + ) + + with pytest.raises(Microsoft365HTTPResponseError) as exception: + Microsoft365Provider.test_connection( + az_cli_auth=True, + raise_on_exception=True, + ) + + assert exception.type == Microsoft365HTTPResponseError + assert ( + exception.value.args[0] + == "[6003] Error in HTTP response from Microsoft365 - Simulated HttpResponseError" + ) + + def test_test_connection_with_exception(self): + with patch( + "prowler.providers.microsoft365.microsoft365_provider.Microsoft365Provider.setup_session" + ) as mock_setup_session: + mock_setup_session.side_effect = Exception("Simulated Exception") + + with pytest.raises(Exception) as exception: + Microsoft365Provider.test_connection( + sp_env_auth=True, + raise_on_exception=True, + ) + + assert exception.type is Exception + assert exception.value.args[0] == "Simulated Exception" + + def test_test_connection_without_any_method(self): + with pytest.raises(Microsoft365NoAuthenticationMethodError) as exception: + Microsoft365Provider.test_connection() + + assert exception.type == Microsoft365NoAuthenticationMethodError + assert ( + "Microsoft365 provider requires at least one authentication method set: [--az-cli-auth | --sp-env-auth | --browser-auth]" + in exception.value.args[0] + ) From 270e89f7faef69de841e2ca00b88caf9a6d95b76 Mon Sep 17 00:00:00 2001 From: HugoPBrito Date: Fri, 24 Jan 2025 10:25:30 +0100 Subject: [PATCH 38/44] feat: enhance logic and add m365 static credentials --- .../microsoft365/microsoft365_provider.py | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index 4c60cff228b..610f76c672f 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -382,16 +382,28 @@ def setup_session( """ if not browser_auth: + if sp_env_auth: + try: + Microsoft365Provider.check_service_principal_creds_env_vars() + credentials = ClientSecretCredential( + tenant_id=getenv("M365_TENANT_ID"), + client_id=getenv("M365_CLIENT_ID"), + client_secret=getenv("M365_CLIENT_SECRET"), + ) + except ( + Microsoft365EnvironmentVariableError + ) as environment_credentials_error: + logger.critical( + f"{environment_credentials_error.__class__.__name__}[{environment_credentials_error.__traceback__.tb_lineno}] -- {environment_credentials_error}" + ) + raise environment_credentials_error try: - if ( - sp_env_auth - and Microsoft365Provider.check_application_creds_env_vars() - ): + if microsoft365_credentials: try: credentials = ClientSecretCredential( - tenant_id=getenv("M365_TENANT_ID"), - client_id=getenv("M365_CLIENT_ID"), - client_secret=getenv("M365_CLIENT_SECRET"), + tenant_id=microsoft365_credentials["tenant_id"], + client_id=microsoft365_credentials["client_id"], + client_secret=microsoft365_credentials["client_secret"], ) return credentials except ClientAuthenticationError as error: @@ -660,7 +672,7 @@ def test_connection( return Connection(error=error) @staticmethod - def check_application_creds_env_vars(): + def check_service_principal_creds_env_vars(): """ Checks the presence of required environment variables for application authentication against Microsoft365. @@ -669,24 +681,20 @@ def check_application_creds_env_vars(): - M365_TENANT_ID: Microsoft365 tenant ID - M365_CLIENT_SECRET: Microsoft365 client secret - Returns: - bool: True if all environment variables are present, False otherwise. + If any of the environment variables is missing, it logs a critical error and exits the program. """ logger.info( "Microsoft365 provider: checking service principal environment variables ..." ) - missing_env_vars = [] for env_var in ["M365_CLIENT_ID", "M365_TENANT_ID", "M365_CLIENT_SECRET"]: if not getenv(env_var): - missing_env_vars.append(env_var) - - if missing_env_vars: - raise Microsoft365CredentialsUnavailableError( - file=os.path.basename(__file__), - message=f"Missing environment variables needed to authenticate against Microsoft365: {', '.join(missing_env_vars)}", - ) - else: - return True + logger.critical( + f"Microsoft365 provider: Missing environment variable {env_var} needed to authenticate against Microsoft365" + ) + raise Microsoft365EnvironmentVariableError( + file=os.path.basename(__file__), + message=f"Missing environment variable {env_var} required to authenticate.", + ) def setup_identity( self, @@ -751,10 +759,10 @@ async def get_microsoft365_identity(): f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" ) # since that exception is not considered as critical, we keep filling another identity fields - # The id of the sp can be retrieved from environment variables if sp_env_auth or client_id: + # The id of the sp can be retrieved from environment variables identity.identity_id = getenv("M365_CLIENT_ID") - identity.identity_type = "Application" + identity.identity_type = "Service Principal" # Same here, if user can access AAD, some fields are retrieved if not, default value, for az cli # should work but it doesn't, pending issue else: From 3120e66ff6be8c2aa2cb05e9a39441d41e689bd2 Mon Sep 17 00:00:00 2001 From: HugoPBrito Date: Fri, 24 Jan 2025 11:33:20 +0100 Subject: [PATCH 39/44] feat: add resource metadata --- prowler/lib/check/models.py | 19 +++++++++++++------ ...dmincenter_groups_not_public_visibility.py | 2 +- ..._users_admins_reduced_license_footprint.py | 4 +++- ...sers_between_two_and_four_global_admins.py | 4 +++- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index ba5e0bd337a..2aca0e83c18 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -537,18 +537,25 @@ def __init__(self, metadata: Dict, resource: Any) -> None: @dataclass class Check_Report_Microsoft365(Check_Report): - # TODO change class name to CheckReportMicrosoft365 """Contains the Microsoft365 Check's finding information.""" resource_name: str resource_id: str location: str - def __init__(self, metadata): - super().__init__(metadata) - self.resource_name = "" - self.resource_id = "" - self.location = "global" + def __init__(self, metadata: Dict, resource: Any) -> None: + """Initialize the Microsoft365 Check's finding information. + + Args: + metadata: The metadata of the check. + resource: Basic information about the resource. Defaults to None. + """ + super().__init__(metadata, resource) + self.resource_name = getattr( + resource, "name", getattr(resource, "resource_name", "") + ) + self.resource_id = getattr(resource, "id", getattr(resource, "resource_id", "")) + self.location = getattr(resource, "location", "global") # Testing Pending diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py index 24543379e1a..aea037c8a1d 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_groups_not_public_visibility/admincenter_groups_not_public_visibility.py @@ -8,7 +8,7 @@ class admincenter_groups_not_public_visibility(Check): def execute(self) -> Check_Report_Microsoft365: findings = [] for group in admincenter_client.groups.values(): - report = Check_Report_Microsoft365(self.metadata()) + report = Check_Report_Microsoft365(metadata=self.metadata(), resource=group) report.resource_id = group.id report.resource_name = group.name report.status = "FAIL" diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py index 8ffe1067243..0adc464c36b 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_admins_reduced_license_footprint/admincenter_users_admins_reduced_license_footprint.py @@ -18,7 +18,9 @@ def execute(self) -> Check_Report_Microsoft365: ) if admin_roles: - report = Check_Report_Microsoft365(self.metadata()) + report = Check_Report_Microsoft365( + metadata=self.metadata(), resource=user + ) report.resource_id = user.id report.resource_name = user.name report.status = "FAIL" diff --git a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py index c22d022d4a4..aa0fc978c03 100644 --- a/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py +++ b/prowler/providers/microsoft365/services/admincenter/admincenter_users_between_two_and_four_global_admins/admincenter_users_between_two_and_four_global_admins.py @@ -9,7 +9,9 @@ def execute(self) -> Check_Report_Microsoft365: findings = [] directory_roles = admincenter_client.directory_roles - report = Check_Report_Microsoft365(self.metadata()) + report = Check_Report_Microsoft365( + metadata=self.metadata(), resource=admincenter_client.directory_roles + ) report.status = "FAIL" report.resource_name = "Global Administrator" From e38f6e1d863efb38d6b6e5ad8004067e83c40fd9 Mon Sep 17 00:00:00 2001 From: HugoPBrito Date: Fri, 24 Jan 2025 16:40:37 +0100 Subject: [PATCH 40/44] feat: enhance exceptions --- prowler/providers/common/provider.py | 2 +- .../microsoft365/exceptions/exceptions.py | 20 +++++++++++++++++++ .../microsoft365/microsoft365_provider.py | 3 +-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index fd4015ada16..ec9c5de0dbf 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -216,11 +216,11 @@ def init_global_provider(arguments: Namespace) -> None: region=arguments.region, config_path=arguments.config_file, mutelist_path=arguments.mutelist_file, - fixer_config=fixer_config, sp_env_auth=arguments.sp_env_auth, az_cli_auth=arguments.az_cli_auth, browser_auth=arguments.browser_auth, tenant_id=arguments.tenant_id, + fixer_config=fixer_config, ) except TypeError as error: diff --git a/prowler/providers/microsoft365/exceptions/exceptions.py b/prowler/providers/microsoft365/exceptions/exceptions.py index 8d3b49c796f..4fd409c0a88 100644 --- a/prowler/providers/microsoft365/exceptions/exceptions.py +++ b/prowler/providers/microsoft365/exceptions/exceptions.py @@ -74,6 +74,26 @@ class Microsoft365BaseException(ProwlerException): "message": "Error setting up session", "remediation": "Check the session setup and ensure it is properly set up.", }, + (6017, "Microsoft365DefaultAzureCredentialError"): { + "message": "Error with DefaultAzureCredential", + "remediation": "Ensure DefaultAzureCredential is correctly configured.", + }, + (6018, "Microsoft365InteractiveBrowserCredentialError"): { + "message": "Error with InteractiveBrowserCredential", + "remediation": "Ensure InteractiveBrowserCredential is correctly configured.", + }, + (6019, "Microsoft365BrowserAuthNoTenantIDError"): { + "message": "Microsoft365 Tenant ID (--tenant-id) is required for browser authentication mode", + "remediation": "Check the Microsoft365 Tenant ID and ensure it is properly set up.", + }, + (6020, "Microsoft365BrowserAuthNoFlagError"): { + "message": "Microsoft365 provider requires at least one authentication method set: [--az-cli-auth | --sp-env-auth | --browser-auth]", + "remediation": "Check the Microsoft365 authentication methods and ensure at least one is properly set up.", + }, + (6021, "Microsoft365NotTenantIdButClientIdAndClienSecretError"): { + "message": "Tenant Id is required for Microsoft365 static credentials. Make sure you are using the correct credentials.", + "remediation": "Check the Microsoft365 Tenant ID and ensure it is properly set up.", + }, } def __init__(self, code, file=None, original_exception=None, message=None): diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index 610f76c672f..719257d67b1 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -27,7 +27,6 @@ from prowler.providers.common.provider import Provider from prowler.providers.microsoft365.exceptions.exceptions import ( Microsoft365ArgumentTypeValidationError, - Microsoft365BrowserAuthNoFlagError, Microsoft365BrowserAuthNoTenantIDError, Microsoft365ClientAuthenticationError, Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError, @@ -267,7 +266,7 @@ def validate_arguments( if not client_id and not client_secret: if not browser_auth and tenant_id: - raise Microsoft365BrowserAuthNoFlagError( + raise Microsoft365BrowserAuthNoTenantIDError( file=os.path.basename(__file__), message="Microsoft365 Tenant ID (--browser-auth) is required for browser authentication mode", ) From 7bbdeaf37bfdb7476adb1bde43b52bc8d9d829f5 Mon Sep 17 00:00:00 2001 From: HugoPBrito Date: Fri, 24 Jan 2025 16:50:31 +0100 Subject: [PATCH 41/44] fix: browser exceptions --- prowler/providers/microsoft365/exceptions/exceptions.py | 4 ++-- prowler/providers/microsoft365/microsoft365_provider.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/prowler/providers/microsoft365/exceptions/exceptions.py b/prowler/providers/microsoft365/exceptions/exceptions.py index 4fd409c0a88..b1da4f1fbc0 100644 --- a/prowler/providers/microsoft365/exceptions/exceptions.py +++ b/prowler/providers/microsoft365/exceptions/exceptions.py @@ -87,8 +87,8 @@ class Microsoft365BaseException(ProwlerException): "remediation": "Check the Microsoft365 Tenant ID and ensure it is properly set up.", }, (6020, "Microsoft365BrowserAuthNoFlagError"): { - "message": "Microsoft365 provider requires at least one authentication method set: [--az-cli-auth | --sp-env-auth | --browser-auth]", - "remediation": "Check the Microsoft365 authentication methods and ensure at least one is properly set up.", + "message": "Microsoft365 tenant ID error: browser authentication flag (--browser-auth) not found", + "remediation": "To use browser authentication, ensure the tenant ID is properly set.", }, (6021, "Microsoft365NotTenantIdButClientIdAndClienSecretError"): { "message": "Tenant Id is required for Microsoft365 static credentials. Make sure you are using the correct credentials.", diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index 719257d67b1..d32201e0759 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -27,6 +27,7 @@ from prowler.providers.common.provider import Provider from prowler.providers.microsoft365.exceptions.exceptions import ( Microsoft365ArgumentTypeValidationError, + Microsoft365BrowserAuthNoFlagError, Microsoft365BrowserAuthNoTenantIDError, Microsoft365ClientAuthenticationError, Microsoft365ClientIdAndClientSecretNotBelongingToTenantIdError, @@ -266,9 +267,9 @@ def validate_arguments( if not client_id and not client_secret: if not browser_auth and tenant_id: - raise Microsoft365BrowserAuthNoTenantIDError( + raise Microsoft365BrowserAuthNoFlagError( file=os.path.basename(__file__), - message="Microsoft365 Tenant ID (--browser-auth) is required for browser authentication mode", + message="Microsoft365 tenant ID error: browser authentication flag (--browser-auth) not found", ) elif not az_cli_auth and not sp_env_auth and not browser_auth: raise Microsoft365NoAuthenticationMethodError( From e8c803caa76979e699d918843b5b023aef07c681 Mon Sep 17 00:00:00 2001 From: MrCloudSec Date: Fri, 24 Jan 2025 10:52:13 -0500 Subject: [PATCH 42/44] chore: revision --- prowler/providers/microsoft365/microsoft365_provider.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index d32201e0759..06e568b7b0f 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -427,7 +427,9 @@ def setup_session( raise Microsoft365ConfigCredentialsError( file=os.path.basename(__file__), original_exception=error ) - elif az_cli_auth: + else: + # Since the authentication method to be used will come as True, we have to negate it since + # DefaultAzureCredential sets just one authentication method, excluding the others try: credentials = DefaultAzureCredential( exclude_environment_credential=True, From bdd3ef0ecad5a6e2686b5457b2208f8ecf0f9d70 Mon Sep 17 00:00:00 2001 From: MrCloudSec Date: Fri, 24 Jan 2025 11:43:39 -0500 Subject: [PATCH 43/44] chore: revision --- prowler/providers/microsoft365/microsoft365_provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index 06e568b7b0f..0db6304111a 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -432,7 +432,7 @@ def setup_session( # DefaultAzureCredential sets just one authentication method, excluding the others try: credentials = DefaultAzureCredential( - exclude_environment_credential=True, + exclude_environment_credential=not sp_env_auth, exclude_cli_credential=not az_cli_auth, # Microsoft365 Auth using Managed Identity is not supported exclude_managed_identity_credential=True, From b377afd129b7c0bd22ae342026ad0ca40cf68d0f Mon Sep 17 00:00:00 2001 From: MrCloudSec Date: Fri, 24 Jan 2025 12:12:44 -0500 Subject: [PATCH 44/44] chore: revision --- .../microsoft365/lib/arguments/arguments.py | 4 ++-- .../microsoft365/microsoft365_provider.py | 17 ++++++----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/prowler/providers/microsoft365/lib/arguments/arguments.py b/prowler/providers/microsoft365/lib/arguments/arguments.py index d0a5dd771d0..d3e38642e79 100644 --- a/prowler/providers/microsoft365/lib/arguments/arguments.py +++ b/prowler/providers/microsoft365/lib/arguments/arguments.py @@ -20,12 +20,12 @@ def init_parser(self): microsoft365_auth_modes_group.add_argument( "--sp-env-auth", action="store_true", - help="Use Service Principal environment variables authentication to log in against Microsoft365", + help="Use Azure Service Principal environment variables authentication to log in against Microsoft365", ) microsoft365_auth_modes_group.add_argument( "--browser-auth", action="store_true", - help="Use interactive browser authentication to log in against Microsoft365", + help="Use Azure interactive browser authentication to log in against Microsoft365", ) microsoft365_parser.add_argument( "--tenant-id", diff --git a/prowler/providers/microsoft365/microsoft365_provider.py b/prowler/providers/microsoft365/microsoft365_provider.py index 0db6304111a..dff8da4d978 100644 --- a/prowler/providers/microsoft365/microsoft365_provider.py +++ b/prowler/providers/microsoft365/microsoft365_provider.py @@ -385,11 +385,6 @@ def setup_session( if sp_env_auth: try: Microsoft365Provider.check_service_principal_creds_env_vars() - credentials = ClientSecretCredential( - tenant_id=getenv("M365_TENANT_ID"), - client_id=getenv("M365_CLIENT_ID"), - client_secret=getenv("M365_CLIENT_SECRET"), - ) except ( Microsoft365EnvironmentVariableError ) as environment_credentials_error: @@ -676,19 +671,19 @@ def test_connection( @staticmethod def check_service_principal_creds_env_vars(): """ - Checks the presence of required environment variables for application authentication against Microsoft365. + Checks the presence of required environment variables for service principal authentication against Azure. This method checks for the presence of the following environment variables: - - M365_CLIENT_ID: Microsoft365 client ID - - M365_TENANT_ID: Microsoft365 tenant ID - - M365_CLIENT_SECRET: Microsoft365 client secret + - AZURE_CLIENT_ID: Azure client ID + - AZURE_TENANT_ID: Azure tenant ID + - AZURE_CLIENT_SECRET: Azure client secret If any of the environment variables is missing, it logs a critical error and exits the program. """ logger.info( "Microsoft365 provider: checking service principal environment variables ..." ) - for env_var in ["M365_CLIENT_ID", "M365_TENANT_ID", "M365_CLIENT_SECRET"]: + for env_var in ["AZURE_CLIENT_ID", "AZURE_TENANT_ID", "AZURE_CLIENT_SECRET"]: if not getenv(env_var): logger.critical( f"Microsoft365 provider: Missing environment variable {env_var} needed to authenticate against Microsoft365" @@ -763,7 +758,7 @@ async def get_microsoft365_identity(): # since that exception is not considered as critical, we keep filling another identity fields if sp_env_auth or client_id: # The id of the sp can be retrieved from environment variables - identity.identity_id = getenv("M365_CLIENT_ID") + identity.identity_id = getenv("AZURE_CLIENT_ID") identity.identity_type = "Service Principal" # Same here, if user can access AAD, some fields are retrieved if not, default value, for az cli # should work but it doesn't, pending issue