Compare commits

...

1 Commits

Author SHA1 Message Date
Hugo P.Brito
fda4818063 feat(m365): add exchange_organization_delicensing_resiliency_enabled security check
Add new security check exchange_organization_delicensing_resiliency_enabled for m365 provider.
Includes check implementation, fixer, metadata, and unit tests.
The check is license-aware: returns PASS when the feature is enabled or the
tenant has fewer than 5000 total licenses, and MANUAL otherwise.
2026-04-08 15:35:41 +01:00
10 changed files with 614 additions and 0 deletions

View File

@@ -12,6 +12,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `awslambda_function_no_dead_letter_queue`, `awslambda_function_using_cross_account_layers`, and `awslambda_function_env_vars_not_encrypted_with_cmk` checks for AWS Lambda [(#10381)](https://github.com/prowler-cloud/prowler/pull/10381)
- `entra_conditional_access_policy_mdm_compliant_device_required` check for M365 provider [(#10220)](https://github.com/prowler-cloud/prowler/pull/10220)
- `directory_super_admin_only_admin_roles` check for Google Workspace provider [(#10488)](https://github.com/prowler-cloud/prowler/pull/10488)
- `exchange_organization_delicensing_resiliency_enabled` check for m365 provider [(#10608)](https://github.com/prowler-cloud/prowler/pull/10608)
- `ec2_securitygroup_allow_ingress_from_internet_to_any_port_from_ip` check for AWS provider using `ipaddress.is_global` for accurate public IP detection [(#10335)](https://github.com/prowler-cloud/prowler/pull/10335)
- `entra_conditional_access_policy_block_o365_elevated_insider_risk` check for M365 provider [(#10232)](https://github.com/prowler-cloud/prowler/pull/10232)
- `--resource-group` and `--list-resource-groups` CLI flags to filter checks by resource group across all providers [(#10479)](https://github.com/prowler-cloud/prowler/pull/10479)

View File

@@ -425,6 +425,7 @@
],
"Checks": [
"admincenter_groups_not_public_visibility",
"exchange_organization_delicensing_resiliency_enabled",
"teams_meeting_recording_disabled"
]
},

View File

@@ -0,0 +1,37 @@
{
"Provider": "m365",
"CheckID": "exchange_organization_delicensing_resiliency_enabled",
"CheckTitle": "Delicensing Resiliency protects Exchange Online mailboxes from immediate access loss during license changes",
"CheckType": [],
"ServiceName": "exchange",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "NotDefined",
"ResourceGroup": "collaboration",
"Description": "**Microsoft 365 Exchange Online** Delicensing Resiliency provides a grace period when licenses expire or are reassigned, preventing immediate mailbox access loss.\n\nThis evaluates whether the organization has **Delayed Delicensing** enabled to protect mailbox data during licensing transitions. Note: This feature is only available to tenants with 5000 or more paid licenses.",
"Risk": "Without **Delicensing Resiliency**, removing or reassigning an Exchange Online license causes **immediate mailbox inaccessibility**. This can lead to data loss, business disruption, and inability to recover mailbox contents during organizational changes such as role transitions or license optimizations.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/exchange/recipients-in-exchange-online/manage-user-mailboxes/delicensing-resiliency",
"https://maester.dev/docs/tests/MT.1083"
],
"Remediation": {
"Code": {
"CLI": "Set-OrganizationConfig -DelayedDelicensingEnabled $true",
"NativeIaC": "",
"Other": "1. Connect to Exchange Online PowerShell\n2. Run: Set-OrganizationConfig -DelayedDelicensingEnabled $true\n3. Verify with: Get-OrganizationConfig | Format-List DelayedDelicensingEnabled",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable **Delicensing Resiliency** to ensure mailbox data is preserved during license transitions. This provides a grace period allowing administrators to reassign licenses or export data before access is permanently revoked, maintaining **business continuity** and **data protection**.",
"Url": "https://hub.prowler.com/check/exchange_organization_delicensing_resiliency_enabled"
}
},
"Categories": [
"resilience"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "This check includes an automated fixer that runs `Set-OrganizationConfig -DelayedDelicensingEnabled $true` via Exchange Online PowerShell. Delicensing Resiliency is only available to tenants with 5000 or more paid licenses; tenants below this threshold are automatically marked as compliant."
}

View File

@@ -0,0 +1,76 @@
"""Check for Exchange Online Delicensing Resiliency configuration."""
from typing import List
from prowler.lib.check.models import Check, CheckReportM365
from prowler.providers.m365.services.exchange.exchange_client import exchange_client
DELICENSING_LICENSE_THRESHOLD = 5000
class exchange_organization_delicensing_resiliency_enabled(Check):
"""
Check if Delicensing Resiliency is enabled for Exchange Online.
Delicensing Resiliency provides a grace period when licenses expire or are
reassigned, preventing immediate mailbox access loss and allowing
organizations time to manage licensing transitions.
This feature is only available to tenants with 5000 or more paid licenses.
Attributes:
metadata: Metadata associated with the check (inherited from Check).
"""
def execute(self) -> List[CheckReportM365]:
"""
Execute the check for Delicensing Resiliency in Exchange Online.
Iterates over the Exchange Online organization configuration and
evaluates whether Delicensing Resiliency is enabled, taking into
account the tenant's paid license count.
Returns:
List[CheckReportM365]: A list of reports containing the result of the check.
"""
findings = []
organization_config = exchange_client.organization_config
if organization_config:
report = CheckReportM365(
metadata=self.metadata(),
resource=organization_config,
resource_name=organization_config.name,
resource_id=organization_config.guid,
)
if organization_config.delayed_delicensing_enabled:
report.status = "PASS"
report.status_extended = (
"Delicensing Resiliency is enabled for Exchange Online, "
"providing a grace period when licenses are removed."
)
elif (
organization_config.total_paid_licenses is not None
and organization_config.total_paid_licenses
< DELICENSING_LICENSE_THRESHOLD
):
report.status = "PASS"
report.status_extended = (
f"Delicensing Resiliency is not applicable for this tenant. "
f"The tenant has {organization_config.total_paid_licenses} "
f"total licenses, which is below the "
f"{DELICENSING_LICENSE_THRESHOLD} paid license threshold "
f"required by Microsoft for this feature."
)
else:
report.status = "MANUAL"
report.status_extended = (
"Delicensing Resiliency is not enabled for Exchange Online. "
"This feature is only available to tenants with 5000 or more "
"paid licenses. Verify whether the tenant qualifies and "
"enable Delicensing Resiliency if applicable."
)
findings.append(report)
return findings

View File

@@ -0,0 +1,57 @@
"""Fixer for Exchange Online Delicensing Resiliency."""
from prowler.lib.logger import logger
from prowler.providers.common.provider import Provider
from prowler.providers.m365.lib.powershell.m365_powershell import M365PowerShell
def fixer(resource_id: str = "") -> bool:
"""Enable Delicensing Resiliency in Exchange Online.
Args:
resource_id (str): Unused for this organization-level fixer.
Returns:
bool: True when the fixer command succeeds, False otherwise.
"""
session = None
try:
provider = Provider.get_global_provider()
if not provider:
logger.error("Unable to load the global M365 provider for Exchange Online.")
return False
credentials = getattr(provider, "credentials", None)
identity = getattr(provider, "identity", None)
if not credentials or not identity:
logger.error(
"Unable to load the M365 credentials required for Exchange Online."
)
return False
session = M365PowerShell(credentials, identity)
if not session.connect_exchange_online():
logger.error("Unable to connect to Exchange Online PowerShell.")
return False
result = session.execute(
"Set-OrganizationConfig -DelayedDelicensingEnabled $true",
timeout=30,
)
if result:
logger.error(
"PowerShell execution failed while running "
'"Set-OrganizationConfig -DelayedDelicensingEnabled $true": '
f"{result}"
)
return False
return True
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return False
finally:
if session:
session.close()

View File

@@ -1,3 +1,4 @@
import asyncio
from enum import Enum
from typing import Optional
@@ -47,6 +48,50 @@ class Exchange(M365Service):
self.shared_mailboxes = self._get_shared_mailboxes()
self.powershell.close()
# Fetch license count via Graph API
created_loop = False
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
created_loop = True
if loop.is_closed():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
created_loop = True
if not loop.is_running():
total_paid_licenses = loop.run_until_complete(
self._get_total_paid_licenses()
)
if created_loop:
asyncio.set_event_loop(None)
loop.close()
if self.organization_config is not None:
self.organization_config.total_paid_licenses = total_paid_licenses
async def _get_total_paid_licenses(self) -> Optional[int]:
"""Fetch total paid license count from Microsoft Graph subscribed SKUs."""
logger.info("Microsoft365 - Getting total paid license count...")
try:
subscribed_skus = await self.client.subscribed_skus.get()
total = 0
for sku in getattr(subscribed_skus, "value", []) or []:
prepaid_units = getattr(sku, "prepaid_units", None)
if prepaid_units:
enabled = getattr(prepaid_units, "enabled", 0) or 0
total += enabled
return total
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return None
def _get_organization_config(self):
logger.info("Microsoft365 - Getting Exchange Organization configuration...")
organization_config = None
@@ -74,6 +119,9 @@ class Exchange(M365Service):
mailtips_large_audience_threshold=organization_configuration.get(
"MailTipsLargeAudienceThreshold", 25
),
delayed_delicensing_enabled=organization_configuration.get(
"DelayedDelicensingEnabled", False
),
)
except Exception as error:
logger.error(
@@ -309,6 +357,22 @@ class Exchange(M365Service):
class Organization(BaseModel):
"""
Model for Exchange Online organization configuration.
Attributes:
name: Organization display name.
guid: Organization unique identifier.
audit_disabled: Whether auditing is disabled for the organization.
oauth_enabled: Whether OAuth 2.0 (Modern Authentication) is enabled.
mailtips_enabled: Whether MailTips are enabled.
mailtips_external_recipient_enabled: Whether MailTips for external recipients are enabled.
mailtips_group_metrics_enabled: Whether MailTips group metrics are enabled.
mailtips_large_audience_threshold: Threshold for large audience MailTips.
delayed_delicensing_enabled: Whether Delicensing Resiliency is enabled.
total_paid_licenses: Total paid licenses in the tenant, or None if unknown.
"""
name: str
guid: str
audit_disabled: bool
@@ -317,6 +381,8 @@ class Organization(BaseModel):
mailtips_external_recipient_enabled: bool
mailtips_group_metrics_enabled: bool
mailtips_large_audience_threshold: int
delayed_delicensing_enabled: bool = False
total_paid_licenses: Optional[int] = None
class MailboxAuditConfig(BaseModel):

View File

@@ -0,0 +1,61 @@
from unittest import mock
from tests.providers.m365.m365_fixtures import set_mocked_m365_provider
class Test_exchange_organization_delicensing_resiliency_enabled_fixer:
def test_creates_new_powershell_session(self):
created_session = mock.MagicMock()
created_session.connect_exchange_online.return_value = True
created_session.execute.return_value = ""
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled_fixer.M365PowerShell",
return_value=created_session,
) as mocked_powershell,
):
from prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled_fixer import (
fixer,
)
assert fixer()
mocked_powershell.assert_called_once()
created_session.connect_exchange_online.assert_called_once()
created_session.execute.assert_any_call(
"Set-OrganizationConfig -DelayedDelicensingEnabled $true",
timeout=30,
)
created_session.close.assert_called_once()
def test_logs_power_shell_execution_error(self):
created_session = mock.MagicMock()
created_session.connect_exchange_online.return_value = True
created_session.execute.return_value = "Access is denied."
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled_fixer.M365PowerShell",
return_value=created_session,
),
mock.patch(
"prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled_fixer.logger.error",
) as mocked_logger_error,
):
from prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled_fixer import (
fixer,
)
assert not fixer()
mocked_logger_error.assert_any_call(
'PowerShell execution failed while running "Set-OrganizationConfig -DelayedDelicensingEnabled $true": Access is denied.'
)
created_session.close.assert_called_once()

View File

@@ -0,0 +1,278 @@
from unittest import mock
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
ORGANIZATION_KWARGS = dict(
name="test-org",
guid="org-guid",
audit_disabled=False,
oauth_enabled=True,
mailtips_enabled=True,
mailtips_external_recipient_enabled=False,
mailtips_group_metrics_enabled=True,
mailtips_large_audience_threshold=25,
)
class Test_exchange_organization_delicensing_resiliency_enabled:
def test_no_organization(self):
exchange_client = mock.MagicMock()
exchange_client.audited_tenant = "audited_tenant"
exchange_client.audited_domain = DOMAIN
exchange_client.organization_config = None
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
),
mock.patch(
"prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled import (
exchange_organization_delicensing_resiliency_enabled,
)
check = exchange_organization_delicensing_resiliency_enabled()
result = check.execute()
assert len(result) == 0
def test_delicensing_resiliency_disabled_above_threshold(self):
"""Disabled + >= 5000 total licenses -> MANUAL (can't confirm paid count)."""
exchange_client = mock.MagicMock()
exchange_client.audited_tenant = "audited_tenant"
exchange_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
),
mock.patch(
"prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled import (
exchange_organization_delicensing_resiliency_enabled,
)
from prowler.providers.m365.services.exchange.exchange_service import (
Organization,
)
exchange_client.organization_config = Organization(
**ORGANIZATION_KWARGS,
delayed_delicensing_enabled=False,
total_paid_licenses=6000,
)
check = exchange_organization_delicensing_resiliency_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "MANUAL"
assert "Verify whether the tenant qualifies" in result[0].status_extended
assert result[0].resource_name == "test-org"
assert result[0].resource_id == "org-guid"
assert result[0].location == "global"
def test_delicensing_resiliency_disabled_at_threshold(self):
"""Disabled + exactly 5000 total licenses -> MANUAL."""
exchange_client = mock.MagicMock()
exchange_client.audited_tenant = "audited_tenant"
exchange_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
),
mock.patch(
"prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled import (
exchange_organization_delicensing_resiliency_enabled,
)
from prowler.providers.m365.services.exchange.exchange_service import (
Organization,
)
exchange_client.organization_config = Organization(
**ORGANIZATION_KWARGS,
delayed_delicensing_enabled=False,
total_paid_licenses=5000,
)
check = exchange_organization_delicensing_resiliency_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "MANUAL"
def test_delicensing_resiliency_disabled_below_threshold(self):
"""Disabled + < 5000 total licenses -> PASS (not applicable)."""
exchange_client = mock.MagicMock()
exchange_client.audited_tenant = "audited_tenant"
exchange_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
),
mock.patch(
"prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled import (
exchange_organization_delicensing_resiliency_enabled,
)
from prowler.providers.m365.services.exchange.exchange_service import (
Organization,
)
exchange_client.organization_config = Organization(
**ORGANIZATION_KWARGS,
delayed_delicensing_enabled=False,
total_paid_licenses=4999,
)
check = exchange_organization_delicensing_resiliency_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert "not applicable" in result[0].status_extended
assert "4999 total licenses" in result[0].status_extended
def test_delicensing_resiliency_disabled_licenses_unknown(self):
"""Disabled + unknown license count -> MANUAL."""
exchange_client = mock.MagicMock()
exchange_client.audited_tenant = "audited_tenant"
exchange_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
),
mock.patch(
"prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled import (
exchange_organization_delicensing_resiliency_enabled,
)
from prowler.providers.m365.services.exchange.exchange_service import (
Organization,
)
exchange_client.organization_config = Organization(
**ORGANIZATION_KWARGS,
delayed_delicensing_enabled=False,
total_paid_licenses=None,
)
check = exchange_organization_delicensing_resiliency_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "MANUAL"
assert "Verify whether the tenant qualifies" in result[0].status_extended
def test_delicensing_resiliency_enabled(self):
"""Enabled -> PASS regardless of license count."""
exchange_client = mock.MagicMock()
exchange_client.audited_tenant = "audited_tenant"
exchange_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
),
mock.patch(
"prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled import (
exchange_organization_delicensing_resiliency_enabled,
)
from prowler.providers.m365.services.exchange.exchange_service import (
Organization,
)
exchange_client.organization_config = Organization(
**ORGANIZATION_KWARGS,
delayed_delicensing_enabled=True,
total_paid_licenses=6000,
)
check = exchange_organization_delicensing_resiliency_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert "is enabled" in result[0].status_extended
assert result[0].resource == exchange_client.organization_config.dict()
assert result[0].resource_name == "test-org"
assert result[0].resource_id == "org-guid"
assert result[0].location == "global"
def test_delicensing_resiliency_enabled_below_threshold(self):
"""Enabled + below threshold -> still PASS (enabled always wins)."""
exchange_client = mock.MagicMock()
exchange_client.audited_tenant = "audited_tenant"
exchange_client.audited_domain = DOMAIN
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_m365_provider(),
),
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
),
mock.patch(
"prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled.exchange_client",
new=exchange_client,
),
):
from prowler.providers.m365.services.exchange.exchange_organization_delicensing_resiliency_enabled.exchange_organization_delicensing_resiliency_enabled import (
exchange_organization_delicensing_resiliency_enabled,
)
from prowler.providers.m365.services.exchange.exchange_service import (
Organization,
)
exchange_client.organization_config = Organization(
**ORGANIZATION_KWARGS,
delayed_delicensing_enabled=True,
total_paid_licenses=100,
)
check = exchange_organization_delicensing_resiliency_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert "is enabled" in result[0].status_extended

View File

@@ -161,6 +161,18 @@ def mock_exchange_get_shared_mailboxes(_):
]
async def mock_exchange_get_total_paid_licenses(_):
return 6000
async def mock_exchange_get_total_paid_licenses_none(_):
return None
@patch(
"prowler.providers.m365.services.exchange.exchange_service.Exchange._get_total_paid_licenses",
new=mock_exchange_get_total_paid_licenses,
)
class Test_Exchange_Service:
def test_get_client(self):
with (
@@ -201,6 +213,7 @@ class Test_Exchange_Service:
assert organization_config.mailtips_external_recipient_enabled is False
assert organization_config.mailtips_group_metrics_enabled is True
assert organization_config.mailtips_large_audience_threshold == 25
assert organization_config.total_paid_licenses == 6000
exchange_client.powershell.close()
@@ -481,3 +494,27 @@ class Test_Exchange_Service:
assert shared_mailboxes[1].identity == "info@contoso.com"
exchange_client.powershell.close()
def test_get_total_paid_licenses_none(self):
with (
mock.patch(
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
),
mock.patch.object(
Exchange,
"_get_organization_config",
mock_exchange_get_organization_config,
),
mock.patch.object(
Exchange,
"_get_total_paid_licenses",
mock_exchange_get_total_paid_licenses_none,
),
):
exchange_client = Exchange(
set_mocked_m365_provider(
identity=M365IdentityInfo(tenant_domain=DOMAIN)
)
)
assert exchange_client.organization_config.total_paid_licenses is None
exchange_client.powershell.close()