mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
feat(m365): add entra_conditional_access_policy_p1_license_utilization security check
Add new security check entra_conditional_access_policy_p1_license_utilization for m365 provider. Includes check implementation, metadata, and unit tests.
This commit is contained in:
@@ -4,6 +4,10 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
||||
|
||||
## [5.24.1] (Prowler UNRELEASED)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- `entra_conditional_access_policy_p1_license_utilization` check for m365 provider [(#10783)](https://github.com/prowler-cloud/prowler/pull/10783)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- bumped `msgraph-sdk` from 1.23.0 to 1.55.0 and `azure-mgmt-resource` from 23.3.0 to 24.0.0, removing `marshmallow` as is a transitively dev dependency [(#10733)](https://github.com/prowler-cloud/prowler/pull/10733)
|
||||
|
||||
@@ -206,11 +206,12 @@
|
||||
"admincenter_users_admins_reduced_license_footprint",
|
||||
"entra_admin_portals_access_restriction",
|
||||
"entra_admin_users_phishing_resistant_mfa_enabled",
|
||||
"entra_conditional_access_policy_mfa_enforced_for_guest_users",
|
||||
"entra_conditional_access_policy_block_o365_elevated_insider_risk",
|
||||
"entra_conditional_access_policy_block_unknown_device_platforms",
|
||||
"entra_conditional_access_policy_directory_sync_account_excluded",
|
||||
"entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced",
|
||||
"entra_conditional_access_policy_directory_sync_account_excluded",
|
||||
"entra_conditional_access_policy_mfa_enforced_for_guest_users",
|
||||
"entra_conditional_access_policy_p1_license_utilization",
|
||||
"entra_policy_guest_users_access_restrictions",
|
||||
"entra_seamless_sso_disabled"
|
||||
]
|
||||
@@ -248,15 +249,16 @@
|
||||
"defenderxdr_endpoint_privileged_user_exposed_credentials",
|
||||
"entra_admin_users_mfa_enabled",
|
||||
"entra_admin_users_sign_in_frequency_enabled",
|
||||
"entra_break_glass_account_fido2_security_key_registered",
|
||||
"entra_conditional_access_policy_mfa_enforced_for_guest_users",
|
||||
"entra_default_app_management_policy_enabled",
|
||||
"entra_all_apps_conditional_access_coverage",
|
||||
"entra_conditional_access_policy_device_registration_mfa_required",
|
||||
"entra_intune_enrollment_sign_in_frequency_every_time",
|
||||
"entra_break_glass_account_fido2_security_key_registered",
|
||||
"entra_conditional_access_policy_block_unknown_device_platforms",
|
||||
"entra_conditional_access_policy_device_code_flow_blocked",
|
||||
"entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced",
|
||||
"entra_conditional_access_policy_device_code_flow_blocked",
|
||||
"entra_conditional_access_policy_device_registration_mfa_required",
|
||||
"entra_conditional_access_policy_mfa_enforced_for_guest_users",
|
||||
"entra_conditional_access_policy_p1_license_utilization",
|
||||
"entra_default_app_management_policy_enabled",
|
||||
"entra_intune_enrollment_sign_in_frequency_every_time",
|
||||
"entra_legacy_authentication_blocked",
|
||||
"entra_managed_device_required_for_authentication",
|
||||
"entra_seamless_sso_disabled",
|
||||
@@ -718,16 +720,17 @@
|
||||
"entra_admin_users_mfa_enabled",
|
||||
"entra_admin_users_sign_in_frequency_enabled",
|
||||
"entra_all_apps_conditional_access_coverage",
|
||||
"entra_conditional_access_policy_device_registration_mfa_required",
|
||||
"entra_conditional_access_policy_mfa_enforced_for_guest_users",
|
||||
"entra_intune_enrollment_sign_in_frequency_every_time",
|
||||
"entra_break_glass_account_fido2_security_key_registered",
|
||||
"entra_conditional_access_policy_approved_client_app_required_for_mobile",
|
||||
"entra_conditional_access_policy_block_unknown_device_platforms",
|
||||
"entra_conditional_access_policy_device_code_flow_blocked",
|
||||
"entra_conditional_access_policy_directory_sync_account_excluded",
|
||||
"entra_conditional_access_policy_corporate_device_sign_in_frequency_enforced",
|
||||
"entra_conditional_access_policy_device_code_flow_blocked",
|
||||
"entra_conditional_access_policy_device_registration_mfa_required",
|
||||
"entra_conditional_access_policy_directory_sync_account_excluded",
|
||||
"entra_conditional_access_policy_mfa_enforced_for_guest_users",
|
||||
"entra_conditional_access_policy_p1_license_utilization",
|
||||
"entra_identity_protection_sign_in_risk_enabled",
|
||||
"entra_intune_enrollment_sign_in_frequency_every_time",
|
||||
"entra_managed_device_required_for_authentication",
|
||||
"entra_seamless_sso_disabled",
|
||||
"entra_users_mfa_enabled"
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"Provider": "m365",
|
||||
"CheckID": "entra_conditional_access_policy_p1_license_utilization",
|
||||
"CheckTitle": "P1 license entitlements ensure sufficient coverage for Conditional Access users",
|
||||
"CheckType": [],
|
||||
"ServiceName": "entra",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "IAM",
|
||||
"Description": "Verifies that the number of entitled Azure AD Premium **P1 licenses** is sufficient to cover all users actively utilizing **Conditional Access**. Unlicensed Conditional Access usage may violate Microsoft licensing terms and indicates gaps in license governance.",
|
||||
"Risk": "When more users consume Conditional Access than the organization has P1 licenses for, it creates a **licensing compliance gap**. Microsoft may flag the tenant during audits, leading to retroactive charges or service restrictions. Additionally, unlicensed users may lose Conditional Access protection if Microsoft enforces entitlements.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview",
|
||||
"https://learn.microsoft.com/en-us/entra/fundamentals/concept-license-utilization"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. Navigate to the **Microsoft Entra admin center** (https://entra.microsoft.com).\n2. Go to **Identity** > **Billing** > **Licenses** > **Licensed features**.\n3. Review the **License utilization** report for P1 features.\n4. Purchase additional P1 licenses or remove Conditional Access policy assignments for unlicensed users to bring entitlements in line with utilization.",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Review the Azure AD Premium license utilization report and ensure that P1 license entitlements match or exceed the number of users utilizing Conditional Access. Purchase additional licenses or adjust policy scope as needed.",
|
||||
"Url": "https://hub.prowler.com/check/entra_conditional_access_policy_p1_license_utilization"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"identity-access",
|
||||
"e3"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "This check uses the Microsoft Graph beta API endpoint reports/azureADPremiumLicenseInsight. The Reports.Read.All permission is required."
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
from prowler.lib.check.models import Check, CheckReportM365
|
||||
from prowler.providers.m365.services.entra.entra_client import entra_client
|
||||
|
||||
|
||||
class entra_conditional_access_policy_p1_license_utilization(Check):
|
||||
"""Check if P1 license entitlements cover Conditional Access utilization.
|
||||
|
||||
Validates that the number of entitled Azure AD Premium P1 licenses
|
||||
is sufficient to cover all users actively utilizing Conditional Access.
|
||||
|
||||
- PASS: P1 license entitlements cover all Conditional Access users.
|
||||
- FAIL: More users utilize Conditional Access than entitled P1 licenses.
|
||||
"""
|
||||
|
||||
def execute(self) -> list[CheckReportM365]:
|
||||
"""Execute the P1 license utilization check.
|
||||
|
||||
Returns:
|
||||
A list of reports containing the result of the check.
|
||||
"""
|
||||
findings = []
|
||||
|
||||
insight = entra_client.premium_license_insight
|
||||
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=insight if insight else {},
|
||||
resource_name="Premium License Insight",
|
||||
resource_id="azureADPremiumLicenseInsight",
|
||||
)
|
||||
|
||||
if insight is None:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
"Could not retrieve Azure AD Premium license insight data. "
|
||||
"Verify that the tenant has a P1 or P2 license and that "
|
||||
"the required permissions are granted."
|
||||
)
|
||||
elif insight.p1_license_count >= insight.conditional_access_users_count:
|
||||
report.status = "PASS"
|
||||
report.status_extended = (
|
||||
f"P1 license entitlements ({insight.p1_license_count}) cover "
|
||||
f"all Conditional Access users ({insight.conditional_access_users_count})."
|
||||
)
|
||||
else:
|
||||
report.status = "FAIL"
|
||||
report.status_extended = (
|
||||
f"P1 license entitlements ({insight.p1_license_count}) do not cover "
|
||||
f"all Conditional Access users ({insight.conditional_access_users_count})."
|
||||
)
|
||||
|
||||
findings.append(report)
|
||||
return findings
|
||||
@@ -5,6 +5,8 @@ from enum import Enum
|
||||
from typing import Dict, List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from kiota_abstractions.method import Method
|
||||
from kiota_abstractions.request_information import RequestInformation
|
||||
from msgraph.generated.models.o_data_errors.o_data_error import ODataError
|
||||
from msgraph.generated.security.microsoft_graph_security_run_hunting_query.run_hunting_query_post_request_body import (
|
||||
RunHuntingQueryPostRequestBody,
|
||||
@@ -36,6 +38,7 @@ class Entra(M365Service):
|
||||
user_accounts_status (dict): Dictionary of user account statuses.
|
||||
oauth_apps (dict): Dictionary of OAuth applications from Defender XDR.
|
||||
authentication_method_configurations (dict): Dictionary of authentication method configurations.
|
||||
premium_license_insight (PremiumLicenseInsight): Azure AD Premium license utilization insight.
|
||||
"""
|
||||
|
||||
def __init__(self, provider: M365Provider):
|
||||
@@ -83,6 +86,7 @@ class Entra(M365Service):
|
||||
self._get_oauth_apps(),
|
||||
self._get_directory_sync_settings(),
|
||||
self._get_authentication_method_configurations(),
|
||||
self._get_premium_license_insight(),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -98,6 +102,7 @@ class Entra(M365Service):
|
||||
self.authentication_method_configurations: Dict[
|
||||
str, AuthenticationMethodConfiguration
|
||||
] = attributes[9]
|
||||
self.premium_license_insight: Optional[PremiumLicenseInsight] = attributes[10]
|
||||
self.user_accounts_status = {}
|
||||
|
||||
if created_loop:
|
||||
@@ -1019,6 +1024,44 @@ OAuthAppInfo
|
||||
|
||||
return oauth_apps
|
||||
|
||||
async def _get_premium_license_insight(self) -> Optional["PremiumLicenseInsight"]:
|
||||
"""Retrieve Azure AD Premium license insight from the Microsoft Graph beta API.
|
||||
|
||||
Fetches the premium license utilization report which includes the total
|
||||
number of entitled P1 licenses and the number of users actively utilizing
|
||||
Conditional Access (a P1 feature).
|
||||
|
||||
Returns:
|
||||
Optional[PremiumLicenseInsight]: The premium license insight data,
|
||||
or None if the API call failed or data is unavailable.
|
||||
"""
|
||||
logger.info("Entra - Getting premium license insight...")
|
||||
try:
|
||||
request_info = RequestInformation()
|
||||
request_info.http_method = Method.GET
|
||||
request_info.url_template = (
|
||||
"{+baseurl}/reports/azureADPremiumLicenseInsight"
|
||||
)
|
||||
request_info.path_parameters = {
|
||||
"baseurl": "https://graph.microsoft.com/beta"
|
||||
}
|
||||
response = await self.client.request_adapter.send_primitive_async(
|
||||
request_info, "bytes", {}
|
||||
)
|
||||
if response:
|
||||
data = json.loads(response)
|
||||
return PremiumLicenseInsight(
|
||||
p1_license_count=data.get("p1LicenseCount", 0),
|
||||
conditional_access_users_count=data.get(
|
||||
"conditionalAccessUsersCount", 0
|
||||
),
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return None
|
||||
|
||||
async def _get_authentication_method_configurations(self):
|
||||
"""Retrieve authentication method configurations from Microsoft Entra.
|
||||
|
||||
@@ -1481,3 +1524,18 @@ class OAuthApp(BaseModel):
|
||||
is_admin_consented: bool = False
|
||||
last_used_time: Optional[str] = None
|
||||
app_origin: str = ""
|
||||
|
||||
|
||||
class PremiumLicenseInsight(BaseModel):
|
||||
"""Model representing Azure AD Premium license utilization insight.
|
||||
|
||||
Contains the total number of entitled P1 licenses and the number
|
||||
of users actively utilizing Conditional Access, a P1-gated feature.
|
||||
|
||||
Attributes:
|
||||
p1_license_count: Total number of entitled P1 (or P2) licenses.
|
||||
conditional_access_users_count: Number of users utilizing Conditional Access.
|
||||
"""
|
||||
|
||||
p1_license_count: int = 0
|
||||
conditional_access_users_count: int = 0
|
||||
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
from unittest import mock
|
||||
|
||||
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
|
||||
|
||||
CHECK_MODULE_PATH = "prowler.providers.m365.services.entra.entra_conditional_access_policy_p1_license_utilization.entra_conditional_access_policy_p1_license_utilization"
|
||||
|
||||
|
||||
class Test_entra_conditional_access_policy_p1_license_utilization:
|
||||
def test_no_premium_license_insight(self):
|
||||
"""FAIL when premium license insight data is unavailable (None)."""
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
f"{CHECK_MODULE_PATH}.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_conditional_access_policy_p1_license_utilization.entra_conditional_access_policy_p1_license_utilization import (
|
||||
entra_conditional_access_policy_p1_license_utilization,
|
||||
)
|
||||
|
||||
entra_client.premium_license_insight = None
|
||||
|
||||
check = entra_conditional_access_policy_p1_license_utilization()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert "Could not retrieve" in result[0].status_extended
|
||||
assert result[0].resource == {}
|
||||
assert result[0].resource_name == "Premium License Insight"
|
||||
assert result[0].resource_id == "azureADPremiumLicenseInsight"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_licenses_cover_utilization(self):
|
||||
"""PASS when P1 license count >= conditional access users count."""
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
f"{CHECK_MODULE_PATH}.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_conditional_access_policy_p1_license_utilization.entra_conditional_access_policy_p1_license_utilization import (
|
||||
entra_conditional_access_policy_p1_license_utilization,
|
||||
)
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
PremiumLicenseInsight,
|
||||
)
|
||||
|
||||
entra_client.premium_license_insight = PremiumLicenseInsight(
|
||||
p1_license_count=100,
|
||||
conditional_access_users_count=80,
|
||||
)
|
||||
|
||||
check = entra_conditional_access_policy_p1_license_utilization()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "P1 license entitlements (100) cover all Conditional Access users (80)."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== entra_client.premium_license_insight.dict()
|
||||
)
|
||||
assert result[0].resource_name == "Premium License Insight"
|
||||
assert result[0].resource_id == "azureADPremiumLicenseInsight"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_licenses_equal_utilization(self):
|
||||
"""PASS when P1 license count exactly equals conditional access users count."""
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
f"{CHECK_MODULE_PATH}.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_conditional_access_policy_p1_license_utilization.entra_conditional_access_policy_p1_license_utilization import (
|
||||
entra_conditional_access_policy_p1_license_utilization,
|
||||
)
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
PremiumLicenseInsight,
|
||||
)
|
||||
|
||||
entra_client.premium_license_insight = PremiumLicenseInsight(
|
||||
p1_license_count=50,
|
||||
conditional_access_users_count=50,
|
||||
)
|
||||
|
||||
check = entra_conditional_access_policy_p1_license_utilization()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "P1 license entitlements (50) cover all Conditional Access users (50)."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== entra_client.premium_license_insight.dict()
|
||||
)
|
||||
assert result[0].resource_name == "Premium License Insight"
|
||||
assert result[0].resource_id == "azureADPremiumLicenseInsight"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_licenses_insufficient(self):
|
||||
"""FAIL when P1 license count < conditional access users count."""
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
f"{CHECK_MODULE_PATH}.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_conditional_access_policy_p1_license_utilization.entra_conditional_access_policy_p1_license_utilization import (
|
||||
entra_conditional_access_policy_p1_license_utilization,
|
||||
)
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
PremiumLicenseInsight,
|
||||
)
|
||||
|
||||
entra_client.premium_license_insight = PremiumLicenseInsight(
|
||||
p1_license_count=30,
|
||||
conditional_access_users_count=80,
|
||||
)
|
||||
|
||||
check = entra_conditional_access_policy_p1_license_utilization()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "P1 license entitlements (30) do not cover all Conditional Access users (80)."
|
||||
)
|
||||
assert (
|
||||
result[0].resource
|
||||
== entra_client.premium_license_insight.dict()
|
||||
)
|
||||
assert result[0].resource_name == "Premium License Insight"
|
||||
assert result[0].resource_id == "azureADPremiumLicenseInsight"
|
||||
assert result[0].location == "global"
|
||||
|
||||
def test_zero_licenses_zero_users(self):
|
||||
"""PASS when both license count and utilization are zero."""
|
||||
entra_client = mock.MagicMock
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.connect_exchange_online"
|
||||
),
|
||||
mock.patch(
|
||||
f"{CHECK_MODULE_PATH}.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_conditional_access_policy_p1_license_utilization.entra_conditional_access_policy_p1_license_utilization import (
|
||||
entra_conditional_access_policy_p1_license_utilization,
|
||||
)
|
||||
from prowler.providers.m365.services.entra.entra_service import (
|
||||
PremiumLicenseInsight,
|
||||
)
|
||||
|
||||
entra_client.premium_license_insight = PremiumLicenseInsight(
|
||||
p1_license_count=0,
|
||||
conditional_access_users_count=0,
|
||||
)
|
||||
|
||||
check = entra_conditional_access_policy_p1_license_utilization()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert (
|
||||
result[0].status_extended
|
||||
== "P1 license entitlements (0) cover all Conditional Access users (0)."
|
||||
)
|
||||
assert result[0].resource_name == "Premium License Insight"
|
||||
assert result[0].resource_id == "azureADPremiumLicenseInsight"
|
||||
assert result[0].location == "global"
|
||||
Reference in New Issue
Block a user