mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-15 07:54:21 +00:00
feat(m365): add entra_app_registration_redirect_uris_secure check (#12993)
Co-authored-by: kinomakino <3662588+kinomakino@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Pedro Martín <pedromarting3@gmail.com>
This commit is contained in:
8 files changed
+1074
No files matched your search
@@ -0,0 +1 @@
|
||||
`entra_app_registration_redirect_uris_secure` check for M365 provider, flagging app registrations with HTTP, wildcard, malformed or `azurewebsites.net` redirect URIs
|
||||
@@ -174,6 +174,7 @@
|
||||
}
|
||||
],
|
||||
"Checks": [
|
||||
"entra_app_registration_redirect_uris_secure",
|
||||
"entra_conditional_access_policy_app_enforced_restrictions",
|
||||
"exchange_transport_config_smtp_auth_disabled",
|
||||
"exchange_transport_rules_mail_forwarding_disabled",
|
||||
@@ -927,6 +928,7 @@
|
||||
"admincenter_users_admins_reduced_license_footprint",
|
||||
"defender_malware_policy_comprehensive_attachments_filter_applied",
|
||||
"entra_app_registration_no_unused_privileged_permissions",
|
||||
"entra_app_registration_redirect_uris_secure",
|
||||
"entra_policy_restricts_user_consent_for_apps",
|
||||
"entra_thirdparty_integrated_apps_not_allowed",
|
||||
"sharepoint_modern_authentication_required"
|
||||
|
||||
Whitespace-only changes.
+39
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"Provider": "m365",
|
||||
"CheckID": "entra_app_registration_redirect_uris_secure",
|
||||
"CheckTitle": "Secure redirect URIs protect application registration tokens from interception and theft",
|
||||
"CheckType": [],
|
||||
"ServiceName": "entra",
|
||||
"SubServiceName": "",
|
||||
"ResourceIdTemplate": "",
|
||||
"Severity": "medium",
|
||||
"ResourceType": "NotDefined",
|
||||
"ResourceGroup": "IAM",
|
||||
"Description": "Entra ID **application registrations** must use **secure redirect URIs**. This check flags web, SPA and public-client redirect URIs that use plain `http://` on non-loopback hosts, reside on shared platform domains like `azurewebsites.net`, contain wildcards, or are malformed.\n\nTokens are delivered to redirect URIs, so insecure URIs expose them to interception or theft.",
|
||||
"Risk": "An insecure redirect URI allows an attacker to **intercept authorization codes and tokens** in transit (plain HTTP), **redirect them** to a host they control (wildcard or malformed URI), or **claim a dangling subdomain** on a shared platform domain like `azurewebsites.net` to receive valid tokens for the application's users.",
|
||||
"RelatedUrl": "",
|
||||
"AdditionalURLs": [
|
||||
"https://learn.microsoft.com/entra/identity-platform/reply-url",
|
||||
"https://learn.microsoft.com/azure/security/fundamentals/subdomain-takeover",
|
||||
"https://learn.microsoft.com/graph/api/resources/application"
|
||||
],
|
||||
"Remediation": {
|
||||
"Code": {
|
||||
"CLI": "az ad app update --id \"$APP_ID\" --web-redirect-uris \"https://$OWNED_DOMAIN/signin-oidc\"",
|
||||
"NativeIaC": "",
|
||||
"Other": "1. Navigate to Microsoft Entra admin center (https://entra.microsoft.com)\n2. Go to Applications > App registrations\n3. Select the flagged application\n4. Go to Authentication\n5. Replace any `http://` redirect URIs with `https://`\n6. Remove any wildcard URIs and unused or malformed URIs\n7. Move applications off `*.azurewebsites.net` onto a custom domain you own\n8. Delete redirect URIs whose domain no longer resolves in DNS",
|
||||
"Terraform": ""
|
||||
},
|
||||
"Recommendation": {
|
||||
"Text": "Use only `https://` redirect URIs pointing to domains you own and control. Remove **wildcard** entries, replace shared platform domains with **custom domains**, and clean up unused or malformed URIs to prevent token theft via redirect manipulation or subdomain takeover.",
|
||||
"Url": "https://hub.prowler.com/check/entra_app_registration_redirect_uris_secure"
|
||||
}
|
||||
},
|
||||
"Categories": [
|
||||
"internet-exposed",
|
||||
"identity-access"
|
||||
],
|
||||
"DependsOn": [],
|
||||
"RelatedTo": [],
|
||||
"Notes": "DNS resolution checks for dangling domains are not performed by this check due to network egress requirements, non-deterministic results, and false positives on internal-only hostnames. Custom URI schemes used by public clients (msal://, ms-appx-web://, brk-://, urn:ietf:wg:oauth:2.0:oob, com.company.app://) are excluded from validation. Loopback hosts (localhost, 127.0.0.1, [::1]) are allowed over plain HTTP per Microsoft's platform behaviour."
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
"""Check that application registration redirect URIs are secure."""
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from prowler.lib.check.models import Check, CheckReportM365
|
||||
from prowler.providers.m365.services.entra.entra_client import entra_client
|
||||
|
||||
# Custom URI schemes used by public clients that are valid and must not be
|
||||
# flagged as malformed or checked for DNS resolution.
|
||||
_PUBLIC_CLIENT_CUSTOM_SCHEME_PREFIXES = (
|
||||
"msal",
|
||||
"ms-appx-web",
|
||||
"brk-",
|
||||
"com.",
|
||||
"msauth.",
|
||||
)
|
||||
|
||||
# Hosts considered loopback and therefore allowed over plain HTTP.
|
||||
_LOOPBACK_HOSTS = {"localhost", "127.0.0.1", "::1"}
|
||||
|
||||
# Platform domains susceptible to subdomain takeover.
|
||||
_PLATFORM_DOMAINS = (".azurewebsites.net",)
|
||||
|
||||
|
||||
def _is_custom_scheme_uri(uri: str) -> bool:
|
||||
"""Return True if the URI uses a known custom scheme for public clients.
|
||||
|
||||
Args:
|
||||
uri: The redirect URI string.
|
||||
|
||||
Returns:
|
||||
True when the scheme is a recognised custom/native scheme that should
|
||||
not be validated further.
|
||||
"""
|
||||
if ":" not in uri:
|
||||
return False
|
||||
# Compare against the scheme only, never against the rest of the URI.
|
||||
scheme = uri.split(":", 1)[0].lower()
|
||||
if scheme == "urn":
|
||||
return True
|
||||
return any(
|
||||
scheme.startswith(prefix) for prefix in _PUBLIC_CLIENT_CUSTOM_SCHEME_PREFIXES
|
||||
)
|
||||
|
||||
|
||||
def _check_uri(uri: str) -> str | None:
|
||||
"""Evaluate a single redirect URI for insecure patterns.
|
||||
|
||||
Args:
|
||||
uri: The redirect URI string.
|
||||
|
||||
Returns:
|
||||
A reason string when the URI is insecure, or None when it is safe.
|
||||
"""
|
||||
# Wildcards are never legitimate, not even in native schemes, so this
|
||||
# runs first (and before parsing, since wildcards may break urlparse).
|
||||
if "*" in uri:
|
||||
return "contains wildcard"
|
||||
|
||||
# Skip recognised custom schemes (public-client / native).
|
||||
if _is_custom_scheme_uri(uri):
|
||||
return None
|
||||
|
||||
try:
|
||||
parsed = urlparse(uri)
|
||||
except Exception:
|
||||
return "malformed URI"
|
||||
|
||||
scheme = (parsed.scheme or "").lower()
|
||||
host = (parsed.hostname or "").lower()
|
||||
|
||||
# A URI that has no scheme or host is malformed.
|
||||
if not scheme or not host:
|
||||
return "malformed URI"
|
||||
|
||||
# urlparse accepts invalid ports; reading .port is what validates them.
|
||||
try:
|
||||
parsed.port
|
||||
except ValueError:
|
||||
return "malformed URI"
|
||||
|
||||
# Plain HTTP with a non-loopback host.
|
||||
if scheme == "http" and host not in _LOOPBACK_HOSTS:
|
||||
return "uses http:// with non-localhost host"
|
||||
|
||||
# Platform domain susceptible to subdomain takeover.
|
||||
for domain in _PLATFORM_DOMAINS:
|
||||
if host == domain.lstrip(".") or host.endswith(domain):
|
||||
return (
|
||||
f"host on shared platform domain {domain.lstrip('.')}, "
|
||||
"subdomain takeover risk if the App Service is removed"
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class entra_app_registration_redirect_uris_secure(Check):
|
||||
"""Ensure application registration redirect URIs are secure.
|
||||
|
||||
This check evaluates every application registration in the tenant and
|
||||
inspects its web, SPA and public-client redirect URIs for insecure
|
||||
patterns: plain HTTP on non-loopback hosts, hosts on shared platform
|
||||
domains (e.g. azurewebsites.net), wildcard characters, and malformed URIs.
|
||||
|
||||
- PASS: None of the application's redirect URIs match a FAIL condition,
|
||||
or the application has no redirect URIs configured.
|
||||
- FAIL: At least one redirect URI is insecure; every offending URI and its
|
||||
reason are listed in ``status_extended``.
|
||||
"""
|
||||
|
||||
def execute(self) -> list[CheckReportM365]:
|
||||
"""Execute the redirect URI security check.
|
||||
|
||||
Returns:
|
||||
A list of reports containing the result of the check.
|
||||
"""
|
||||
findings: list[CheckReportM365] = []
|
||||
|
||||
for app_id, app in entra_client.app_registrations.items():
|
||||
display_name = app.name or app.app_id
|
||||
report = CheckReportM365(
|
||||
metadata=self.metadata(),
|
||||
resource=app,
|
||||
resource_name=display_name,
|
||||
resource_id=app_id,
|
||||
)
|
||||
|
||||
# Collect all redirect URIs across platforms.
|
||||
all_uris = (
|
||||
app.web_redirect_uris
|
||||
+ app.spa_redirect_uris
|
||||
+ app.public_client_redirect_uris
|
||||
)
|
||||
|
||||
# Evaluate each URI.
|
||||
insecure: list[str] = []
|
||||
for uri in all_uris:
|
||||
reason = _check_uri(uri)
|
||||
if reason:
|
||||
insecure.append(f"{uri} ({reason})")
|
||||
|
||||
if insecure:
|
||||
report.status = "FAIL"
|
||||
total = len(insecure)
|
||||
if total > 5:
|
||||
displayed = ", ".join(insecure[:5])
|
||||
displayed += f" (and {total - 5} more)"
|
||||
else:
|
||||
displayed = ", ".join(insecure)
|
||||
report.status_extended = (
|
||||
f"App registration {display_name} has {total} insecure "
|
||||
f"redirect URI(s): {displayed}."
|
||||
)
|
||||
else:
|
||||
report.status = "PASS"
|
||||
if all_uris:
|
||||
report.status_extended = (
|
||||
f"App registration {display_name} has all redirect URIs secure."
|
||||
)
|
||||
else:
|
||||
report.status_extended = f"App registration {display_name} has no redirect URIs configured."
|
||||
|
||||
findings.append(report)
|
||||
|
||||
return findings
|
||||
@@ -2119,11 +2119,34 @@ OAuthAppInfo
|
||||
)
|
||||
)
|
||||
|
||||
# Extract redirect URIs from web, spa and publicClient
|
||||
web_obj = getattr(app, "web", None)
|
||||
web_redirect_uris = (
|
||||
list(getattr(web_obj, "redirect_uris", []) or [])
|
||||
if web_obj
|
||||
else []
|
||||
)
|
||||
spa_obj = getattr(app, "spa", None)
|
||||
spa_redirect_uris = (
|
||||
list(getattr(spa_obj, "redirect_uris", []) or [])
|
||||
if spa_obj
|
||||
else []
|
||||
)
|
||||
public_client_obj = getattr(app, "public_client", None)
|
||||
public_client_redirect_uris = (
|
||||
list(getattr(public_client_obj, "redirect_uris", []) or [])
|
||||
if public_client_obj
|
||||
else []
|
||||
)
|
||||
|
||||
app_registrations[object_id] = AppRegistration(
|
||||
id=object_id,
|
||||
app_id=app_id,
|
||||
name=getattr(app, "display_name", "") or "",
|
||||
password_credentials=password_credentials,
|
||||
web_redirect_uris=web_redirect_uris,
|
||||
spa_redirect_uris=spa_redirect_uris,
|
||||
public_client_redirect_uris=public_client_redirect_uris,
|
||||
)
|
||||
|
||||
next_link = getattr(app_response, "odata_next_link", None)
|
||||
@@ -2935,9 +2958,16 @@ class AppRegistration(BaseModel):
|
||||
name: The application's display name.
|
||||
password_credentials: List of password credentials (client secrets)
|
||||
registered on the application.
|
||||
web_redirect_uris: Redirect URIs configured for the web platform.
|
||||
spa_redirect_uris: Redirect URIs configured for the SPA platform.
|
||||
public_client_redirect_uris: Redirect URIs configured for the
|
||||
public client (mobile/desktop) platform.
|
||||
"""
|
||||
|
||||
id: str
|
||||
app_id: str = ""
|
||||
name: str = ""
|
||||
password_credentials: List[PasswordCredential] = []
|
||||
web_redirect_uris: List[str] = []
|
||||
spa_redirect_uris: List[str] = []
|
||||
public_client_redirect_uris: List[str] = []
|
||||
+783
@@ -0,0 +1,783 @@
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
from prowler.providers.m365.services.entra.entra_service import AppRegistration
|
||||
from tests.providers.m365.m365_fixtures import DOMAIN, set_mocked_m365_provider
|
||||
|
||||
|
||||
class Test_entra_app_registration_redirect_uris_secure:
|
||||
"""Tests for the entra_app_registration_redirect_uris_secure check."""
|
||||
|
||||
def test_no_app_registrations(self):
|
||||
"""No app registrations in tenant: no findings."""
|
||||
entra_client = mock.MagicMock()
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure import (
|
||||
entra_app_registration_redirect_uris_secure,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {}
|
||||
|
||||
check = entra_app_registration_redirect_uris_secure()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 0
|
||||
|
||||
def test_app_no_redirect_uris(self):
|
||||
"""App with no redirect URIs configured: expected PASS."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Clean App"
|
||||
entra_client = mock.MagicMock()
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure import (
|
||||
entra_app_registration_redirect_uris_secure,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id: AppRegistration(
|
||||
id=app_id,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name,
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_redirect_uris_secure()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert "no redirect URIs configured" in result[0].status_extended
|
||||
assert result[0].resource_id == app_id
|
||||
assert result[0].resource_name == app_name
|
||||
|
||||
def test_app_with_all_secure_uris(self):
|
||||
"""App with only secure redirect URIs: expected PASS."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Secure App"
|
||||
entra_client = mock.MagicMock()
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure import (
|
||||
entra_app_registration_redirect_uris_secure,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id: AppRegistration(
|
||||
id=app_id,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name,
|
||||
web_redirect_uris=[
|
||||
"https://portal.contoso.com/signin-oidc",
|
||||
"https://app.contoso.com/callback",
|
||||
],
|
||||
spa_redirect_uris=["http://localhost:3000"],
|
||||
public_client_redirect_uris=[
|
||||
"msal11111111-1111-1111-1111-111111111111://auth"
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_redirect_uris_secure()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert "all redirect URIs secure" in result[0].status_extended
|
||||
assert result[0].resource_id == app_id
|
||||
assert result[0].resource_name == app_name
|
||||
|
||||
def test_app_with_http_non_localhost_uri(self):
|
||||
"""App with an http:// URI on a non-localhost host: expected FAIL."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Http App"
|
||||
entra_client = mock.MagicMock()
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure import (
|
||||
entra_app_registration_redirect_uris_secure,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id: AppRegistration(
|
||||
id=app_id,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name,
|
||||
web_redirect_uris=[
|
||||
"http://portal.contoso.com/signin-oidc",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_redirect_uris_secure()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert "uses http://" in result[0].status_extended
|
||||
assert "portal.contoso.com" in result[0].status_extended
|
||||
assert result[0].resource_id == app_id
|
||||
assert result[0].resource_name == app_name
|
||||
|
||||
def test_app_with_http_localhost_allowed(self):
|
||||
"""App with http://localhost: expected PASS (loopback is allowed)."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Localhost App"
|
||||
entra_client = mock.MagicMock()
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure import (
|
||||
entra_app_registration_redirect_uris_secure,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id: AppRegistration(
|
||||
id=app_id,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name,
|
||||
spa_redirect_uris=[
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:8080",
|
||||
"http://[::1]:5000",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_redirect_uris_secure()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].resource_id == app_id
|
||||
|
||||
def test_app_with_azurewebsites_uri(self):
|
||||
"""App with a host on *.azurewebsites.net: expected FAIL."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Azure Web App"
|
||||
entra_client = mock.MagicMock()
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure import (
|
||||
entra_app_registration_redirect_uris_secure,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id: AppRegistration(
|
||||
id=app_id,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name,
|
||||
web_redirect_uris=[
|
||||
"https://legacy-portal.azurewebsites.net/signin-oidc",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_redirect_uris_secure()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert "azurewebsites.net" in result[0].status_extended
|
||||
assert result[0].resource_id == app_id
|
||||
assert result[0].resource_name == app_name
|
||||
|
||||
def test_app_with_bare_azurewebsites_domain(self):
|
||||
"""App with host exactly azurewebsites.net (no subdomain): expected FAIL."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Bare Domain App"
|
||||
entra_client = mock.MagicMock()
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure import (
|
||||
entra_app_registration_redirect_uris_secure,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id: AppRegistration(
|
||||
id=app_id,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name,
|
||||
web_redirect_uris=[
|
||||
"https://azurewebsites.net/callback",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_redirect_uris_secure()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert "azurewebsites.net" in result[0].status_extended
|
||||
|
||||
def test_app_with_wildcard_uri(self):
|
||||
"""App with a wildcard in the redirect URI: expected FAIL."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Wildcard App"
|
||||
entra_client = mock.MagicMock()
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure import (
|
||||
entra_app_registration_redirect_uris_secure,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id: AppRegistration(
|
||||
id=app_id,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name,
|
||||
web_redirect_uris=[
|
||||
"https://*.contoso.com/callback",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_redirect_uris_secure()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert "wildcard" in result[0].status_extended
|
||||
assert result[0].resource_id == app_id
|
||||
|
||||
def test_app_with_malformed_uri_no_scheme(self):
|
||||
"""App with a malformed URI (no scheme): expected FAIL."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Malformed App"
|
||||
entra_client = mock.MagicMock()
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure import (
|
||||
entra_app_registration_redirect_uris_secure,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id: AppRegistration(
|
||||
id=app_id,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name,
|
||||
web_redirect_uris=[
|
||||
"just-a-path/callback",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_redirect_uris_secure()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert "malformed URI" in result[0].status_extended
|
||||
assert result[0].resource_id == app_id
|
||||
assert result[0].resource_name == app_name
|
||||
|
||||
def test_app_with_malformed_uri_no_host(self):
|
||||
"""App with a malformed URI (scheme but no host): expected FAIL."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "No Host App"
|
||||
entra_client = mock.MagicMock()
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure import (
|
||||
entra_app_registration_redirect_uris_secure,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id: AppRegistration(
|
||||
id=app_id,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name,
|
||||
web_redirect_uris=[
|
||||
"https:///callback",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_redirect_uris_secure()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert "malformed URI" in result[0].status_extended
|
||||
|
||||
def test_app_with_custom_scheme_public_client(self):
|
||||
"""App with custom scheme public-client URIs: expected PASS."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Native App"
|
||||
entra_client = mock.MagicMock()
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure import (
|
||||
entra_app_registration_redirect_uris_secure,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id: AppRegistration(
|
||||
id=app_id,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name,
|
||||
public_client_redirect_uris=[
|
||||
"msal11111111-1111-1111-1111-111111111111://auth",
|
||||
"ms-appx-web://Microsoft.AAD.BrokerPlugin/1234",
|
||||
"brk-multihub://contoso",
|
||||
"urn:ietf:wg:oauth:2.0:oob",
|
||||
"com.company.app://callback",
|
||||
"msauth.com.contoso.app://auth",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_redirect_uris_secure()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].resource_id == app_id
|
||||
assert result[0].resource_name == app_name
|
||||
|
||||
def test_app_with_multiple_insecure_uris(self):
|
||||
"""App with multiple insecure redirect URIs: expected FAIL listing all."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Legacy Portal"
|
||||
entra_client = mock.MagicMock()
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure import (
|
||||
entra_app_registration_redirect_uris_secure,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id: AppRegistration(
|
||||
id=app_id,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name,
|
||||
web_redirect_uris=[
|
||||
"http://portal.contoso.com/signin-oidc",
|
||||
"https://legacy-portal.azurewebsites.net/signin-oidc",
|
||||
"https://*.contoso.com/callback",
|
||||
],
|
||||
spa_redirect_uris=["http://localhost:3000"],
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_redirect_uris_secure()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert "3 insecure redirect URI(s)" in result[0].status_extended
|
||||
assert result[0].resource_name == app_name
|
||||
|
||||
def test_multiple_apps_mixed_results(self):
|
||||
"""Multiple apps: one secure, one insecure."""
|
||||
app_id_pass = str(uuid4())
|
||||
app_name_pass = "Secure App"
|
||||
app_id_fail = str(uuid4())
|
||||
app_name_fail = "Insecure App"
|
||||
entra_client = mock.MagicMock()
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure import (
|
||||
entra_app_registration_redirect_uris_secure,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id_pass: AppRegistration(
|
||||
id=app_id_pass,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name_pass,
|
||||
web_redirect_uris=["https://app.contoso.com/callback"],
|
||||
),
|
||||
app_id_fail: AppRegistration(
|
||||
id=app_id_fail,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name_fail,
|
||||
web_redirect_uris=[
|
||||
"http://app.insecure.com/callback",
|
||||
],
|
||||
),
|
||||
}
|
||||
|
||||
check = entra_app_registration_redirect_uris_secure()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 2
|
||||
result_pass = next(r for r in result if r.resource_id == app_id_pass)
|
||||
result_fail = next(r for r in result if r.resource_id == app_id_fail)
|
||||
assert result_pass.status == "PASS"
|
||||
assert result_pass.resource_name == app_name_pass
|
||||
assert result_fail.status == "FAIL"
|
||||
assert result_fail.resource_name == app_name_fail
|
||||
|
||||
def test_app_with_more_than_five_insecure_uris_truncated(self):
|
||||
"""App with more than 5 insecure URIs: status_extended is truncated."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Many URIs App"
|
||||
entra_client = mock.MagicMock()
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure import (
|
||||
entra_app_registration_redirect_uris_secure,
|
||||
)
|
||||
|
||||
insecure_uris = [f"http://host{i}.example.com/callback" for i in range(7)]
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id: AppRegistration(
|
||||
id=app_id,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name,
|
||||
web_redirect_uris=insecure_uris,
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_redirect_uris_secure()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert "7 insecure redirect URI(s)" in result[0].status_extended
|
||||
assert "(and 2 more)" in result[0].status_extended
|
||||
|
||||
def test_app_with_exactly_five_insecure_uris_no_truncation(self):
|
||||
"""App with exactly 5 insecure URIs: all displayed, no truncation."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Five URIs App"
|
||||
entra_client = mock.MagicMock()
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure import (
|
||||
entra_app_registration_redirect_uris_secure,
|
||||
)
|
||||
|
||||
insecure_uris = [f"http://host{i}.example.com/callback" for i in range(5)]
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id: AppRegistration(
|
||||
id=app_id,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name,
|
||||
web_redirect_uris=insecure_uris,
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_redirect_uris_secure()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert "5 insecure redirect URI(s)" in result[0].status_extended
|
||||
assert "(and" not in result[0].status_extended
|
||||
|
||||
def test_app_with_wildcard_in_path(self):
|
||||
"""App with a wildcard in the path (not host): expected FAIL."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Wildcard Path App"
|
||||
entra_client = mock.MagicMock()
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure import (
|
||||
entra_app_registration_redirect_uris_secure,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id: AppRegistration(
|
||||
id=app_id,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name,
|
||||
web_redirect_uris=[
|
||||
"https://contoso.com/*/callback",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_redirect_uris_secure()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert "wildcard" in result[0].status_extended
|
||||
|
||||
def test_app_with_insecure_spa_redirect_uri(self):
|
||||
"""App with insecure SPA redirect URI: expected FAIL."""
|
||||
app_id = str(uuid4())
|
||||
app_name = "Insecure SPA"
|
||||
entra_client = mock.MagicMock()
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure import (
|
||||
entra_app_registration_redirect_uris_secure,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id: AppRegistration(
|
||||
id=app_id,
|
||||
app_id=str(uuid4()),
|
||||
name=app_name,
|
||||
spa_redirect_uris=[
|
||||
"http://app.contoso.com:4200",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_redirect_uris_secure()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert "uses http://" in result[0].status_extended
|
||||
|
||||
def test_app_resource_name_falls_back_to_app_id(self):
|
||||
"""App with no display name: resource_name falls back to app_id."""
|
||||
app_id = str(uuid4())
|
||||
app_app_id = str(uuid4())
|
||||
entra_client = mock.MagicMock()
|
||||
entra_client.audited_tenant = "audited_tenant"
|
||||
entra_client.audited_domain = DOMAIN
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
),
|
||||
mock.patch(
|
||||
"prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure.entra_client",
|
||||
new=entra_client,
|
||||
),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure import (
|
||||
entra_app_registration_redirect_uris_secure,
|
||||
)
|
||||
|
||||
entra_client.app_registrations = {
|
||||
app_id: AppRegistration(
|
||||
id=app_id,
|
||||
app_id=app_app_id,
|
||||
name="",
|
||||
)
|
||||
}
|
||||
|
||||
check = entra_app_registration_redirect_uris_secure()
|
||||
result = check.execute()
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert result[0].resource_name == app_app_id
|
||||
assert f"App registration {app_app_id} has" in result[0].status_extended
|
||||
|
||||
def test_check_uri_edge_cases(self):
|
||||
"""Edge cases for the redirect URI evaluation helper."""
|
||||
with mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_m365_provider(),
|
||||
):
|
||||
from prowler.providers.m365.services.entra.entra_app_registration_redirect_uris_secure.entra_app_registration_redirect_uris_secure import (
|
||||
_check_uri,
|
||||
)
|
||||
|
||||
# Loopback hosts are allowed over plain http.
|
||||
assert _check_uri("http://localhost:3000/callback") is None
|
||||
assert _check_uri("http://127.0.0.1:8080/callback") is None
|
||||
assert _check_uri("http://[::1]:8080/callback") is None
|
||||
# Wildcards are flagged even in native schemes.
|
||||
assert _check_uri("com.contoso.app://*") == "contains wildcard"
|
||||
# Prefixes only exempt the scheme, not any string that starts with them.
|
||||
assert _check_uri("msalfoo.example.com/callback") == "malformed URI"
|
||||
assert _check_uri("msal11111111-1111-1111-1111-111111111111://auth") is None
|
||||
assert _check_uri("urn:ietf:wg:oauth:2.0:oob") is None
|
||||
# Invalid ports make the URI malformed.
|
||||
assert (
|
||||
_check_uri("https://example.com:notaport/callback") == "malformed URI"
|
||||
)
|
||||
assert _check_uri("https://example.com:99999/callback") == "malformed URI"
|
||||
# azurewebsites.net explains when the takeover risk applies.
|
||||
reason = _check_uri("https://contoso.azurewebsites.net/signin-oidc")
|
||||
assert "subdomain takeover risk if the App Service is removed" in reason
|
||||
@@ -2019,3 +2019,57 @@ class TestAuthorizationPolicyMsolMapping:
|
||||
policy = asyncio.run(service._get_authorization_policy())
|
||||
|
||||
assert policy.block_msol_powershell is value
|
||||
|
||||
|
||||
class TestAppRegistrationRedirectUris:
|
||||
def test_redirect_uris_are_read_for_every_platform(self):
|
||||
app = SimpleNamespace(
|
||||
id="obj-1",
|
||||
app_id="app-1",
|
||||
display_name="Portal",
|
||||
password_credentials=[],
|
||||
key_credentials=[],
|
||||
web=SimpleNamespace(redirect_uris=["https://portal.contoso.com/signin"]),
|
||||
spa=SimpleNamespace(redirect_uris=["http://localhost:3000"]),
|
||||
public_client=SimpleNamespace(redirect_uris=["msalapp-1://auth"]),
|
||||
)
|
||||
service = Entra.__new__(Entra)
|
||||
service.client = SimpleNamespace(
|
||||
applications=SimpleNamespace(
|
||||
get=AsyncMock(
|
||||
return_value=SimpleNamespace(value=[app], odata_next_link=None)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
apps = asyncio.run(service._get_app_registrations())
|
||||
|
||||
assert apps["obj-1"].web_redirect_uris == ["https://portal.contoso.com/signin"]
|
||||
assert apps["obj-1"].spa_redirect_uris == ["http://localhost:3000"]
|
||||
assert apps["obj-1"].public_client_redirect_uris == ["msalapp-1://auth"]
|
||||
|
||||
def test_missing_platforms_give_empty_lists(self):
|
||||
app = SimpleNamespace(
|
||||
id="obj-2",
|
||||
app_id="app-2",
|
||||
display_name="Daemon",
|
||||
password_credentials=[],
|
||||
key_credentials=[],
|
||||
web=None,
|
||||
spa=None,
|
||||
public_client=None,
|
||||
)
|
||||
service = Entra.__new__(Entra)
|
||||
service.client = SimpleNamespace(
|
||||
applications=SimpleNamespace(
|
||||
get=AsyncMock(
|
||||
return_value=SimpleNamespace(value=[app], odata_next_link=None)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
apps = asyncio.run(service._get_app_registrations())
|
||||
|
||||
assert apps["obj-2"].web_redirect_uris == []
|
||||
assert apps["obj-2"].spa_redirect_uris == []
|
||||
assert apps["obj-2"].public_client_redirect_uris == []
|
||||
Reference in new issue
Block a user