Sync from dev @ 252c1cf

Source: main (252c1cf)
Excluded: live tenant exports, generated artifacts, and dev-only tooling.
This commit is contained in:
2026-04-17 15:57:35 +02:00
commit 17d745bdac
52 changed files with 15601 additions and 0 deletions

View File

@@ -0,0 +1,252 @@
from __future__ import annotations
import importlib.util
import json
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
MODULE_PATH = Path(__file__).resolve().parents[1] / "scripts" / "export_entra_baseline.py"
def load_module():
spec = importlib.util.spec_from_file_location("export_entra_baseline", MODULE_PATH)
if spec is None or spec.loader is None:
raise RuntimeError(f"Unable to load module from {MODULE_PATH}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class ExportEntraBaselineTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.module = load_module()
def _namespace(self, root: Path, fail_on_export_error: str) -> SimpleNamespace:
return SimpleNamespace(
root=str(root),
token="token-value",
include_named_locations="true",
include_authentication_strengths="false",
include_conditional_access="false",
include_enterprise_applications="false",
include_app_registrations="false",
enterprise_app_workers=1,
fail_on_export_error=fail_on_export_error,
previous_snapshot_ref="",
)
def test_requested_export_error_is_fatal_by_default(self) -> None:
with tempfile.TemporaryDirectory() as td:
root = Path(td) / "entra"
root.mkdir(parents=True, exist_ok=True)
args = self._namespace(root=root, fail_on_export_error="true")
with (
patch.object(self.module, "parse_args", return_value=args),
patch.object(self.module, "GraphClient") as graph_client_cls,
):
graph_client = MagicMock()
graph_client.get_object.return_value = ({"value": []}, None)
graph_client.get_collection.return_value = ([], "HTTP 500")
graph_client_cls.return_value = graph_client
result = self.module.main()
self.assertEqual(result, 2)
def test_requested_export_error_can_be_non_fatal_when_disabled(self) -> None:
with tempfile.TemporaryDirectory() as td:
root = Path(td) / "entra"
root.mkdir(parents=True, exist_ok=True)
args = self._namespace(root=root, fail_on_export_error="false")
with (
patch.object(self.module, "parse_args", return_value=args),
patch.object(self.module, "GraphClient") as graph_client_cls,
):
graph_client = MagicMock()
graph_client.get_object.return_value = ({"value": []}, None)
graph_client.get_collection.return_value = ([], "HTTP 500")
graph_client_cls.return_value = graph_client
result = self.module.main()
self.assertEqual(result, 0)
def test_normalize_resolution_error_suppresses_transient_dns_variants(self) -> None:
transient_samples = [
"<urlopen error [Errno -3] Temporary failure in name resolution>",
"Temporary failure resolving 'graph.microsoft.com'",
"Failed to resolve host graph.microsoft.com",
"getaddrinfo failed",
]
for sample in transient_samples:
with self.subTest(sample=sample):
self.assertEqual(self.module.normalize_resolution_error(sample), "")
def test_normalize_resolution_error_keeps_non_transient_http_error(self) -> None:
self.assertEqual(self.module.normalize_resolution_error("HTTP 403"), "HTTP 403")
def test_normalize_branch_name_ignores_unresolved_macro(self) -> None:
self.assertEqual(self.module._normalize_branch_name("$(DRIFT_BRANCH_ENTRA)"), "")
def test_required_resource_resolution_backfills_unresolved_from_previous(self) -> None:
current = [
{
"resourceAppId": "00000003-0000-0000-c000-000000000000",
"resourceDisplayName": "Unresolved",
"permissions": [
{
"id": "perm-id-1",
"type": "Scope",
"value": "",
"displayName": "",
"description": "",
}
],
}
]
previous = [
{
"resourceAppId": "00000003-0000-0000-c000-000000000000",
"resourceDisplayName": "Microsoft Graph",
"permissions": [
{
"id": "perm-id-1",
"type": "Scope",
"value": "User.Read.All",
"displayName": "Read all users' full profiles",
"description": "Allows the app to read full profiles.",
}
],
}
]
merged = self.module._merge_required_resource_access_resolution(current, previous)
self.assertEqual(merged[0]["resourceDisplayName"], "Microsoft Graph")
self.assertEqual(merged[0]["permissions"][0]["value"], "User.Read.All")
self.assertEqual(merged[0]["permissions"][0]["displayName"], "Read all users' full profiles")
unresolved_resources, unresolved_permissions = self.module._count_unresolved_required_permissions(merged)
self.assertEqual(unresolved_resources, 0)
self.assertEqual(unresolved_permissions, 0)
def test_app_role_resolution_backfills_unresolved_from_previous(self) -> None:
current = [
{
"resourceId": "resource-1",
"resourceDisplayName": "Unresolved",
"appRoleId": "role-1",
"appRoleValue": "",
"appRoleDisplayName": "",
"principalType": "ServicePrincipal",
}
]
previous = [
{
"resourceId": "resource-1",
"resourceDisplayName": "Office 365 Exchange Online",
"appRoleId": "role-1",
"appRoleValue": "Exchange.ManageAsApp",
"appRoleDisplayName": "Manage Exchange as application",
"principalType": "ServicePrincipal",
}
]
merged = self.module._merge_app_role_assignments_resolution(current, previous)
self.assertEqual(merged[0]["resourceDisplayName"], "Office 365 Exchange Online")
self.assertEqual(merged[0]["appRoleValue"], "Exchange.ManageAsApp")
self.assertEqual(merged[0]["appRoleDisplayName"], "Manage Exchange as application")
unresolved_resources, unresolved_roles = self.module._count_unresolved_app_role_assignments(merged)
self.assertEqual(unresolved_resources, 0)
self.assertEqual(unresolved_roles, 0)
def test_required_resource_access_uses_direct_appid_fallback_when_filter_returns_empty(self) -> None:
app = {
"requiredResourceAccess": [
{
"resourceAppId": "00000003-0000-0000-c000-000000000000",
"resourceAccess": [
{
"id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d",
"type": "Scope",
}
],
}
]
}
client = MagicMock()
client.get_object.side_effect = [
({"value": []}, None),
(
{
"id": "sp-graph",
"appId": "00000003-0000-0000-c000-000000000000",
"displayName": "Microsoft Graph",
"appRoles": [],
"oauth2PermissionScopes": [
{
"id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d",
"value": "User.Read",
"adminConsentDisplayName": "Sign in and read user profile",
"adminConsentDescription": "Allows sign-in and profile read.",
}
],
},
None,
),
]
resolved, unresolved_resources, unresolved_permissions, lookup_errors = self.module.resolve_required_resource_access(
app=app,
client=client,
resource_sp_by_appid={},
)
self.assertEqual(unresolved_resources, 0)
self.assertEqual(unresolved_permissions, 0)
self.assertEqual(lookup_errors, [])
self.assertEqual(resolved[0]["resourceDisplayName"], "Microsoft Graph")
self.assertEqual(resolved[0]["permissions"][0]["value"], "User.Read")
def test_load_resource_sp_cache_from_export_reads_enterprise_apps(self) -> None:
with tempfile.TemporaryDirectory() as td:
root = Path(td) / "entra"
export_dir = root / "Enterprise Applications"
export_dir.mkdir(parents=True, exist_ok=True)
payload = {
"id": "sp-graph",
"appId": "00000003-0000-0000-c000-000000000000",
"displayName": "Microsoft Graph",
"appRoles": [{"id": "role-1", "value": "Directory.Read.All"}],
"oauth2PermissionScopes": [{"id": "scope-1", "value": "User.Read"}],
}
(export_dir / "Microsoft Graph__sp-graph.json").write_text(json.dumps(payload), encoding="utf-8")
cache = self.module._load_resource_sp_cache_from_export(root)
self.assertIn("00000003-0000-0000-c000-000000000000", cache)
graph = cache["00000003-0000-0000-c000-000000000000"]
self.assertEqual(graph["displayName"], "Microsoft Graph")
self.assertEqual(graph["appRoles"][0]["value"], "Directory.Read.All")
self.assertEqual(graph["oauth2PermissionScopes"][0]["value"], "User.Read")
def test_load_resource_sp_cache_from_export_ignores_invalid_files(self) -> None:
with tempfile.TemporaryDirectory() as td:
root = Path(td) / "entra"
export_dir = root / "Enterprise Applications"
export_dir.mkdir(parents=True, exist_ok=True)
(export_dir / "invalid.json").write_text("{", encoding="utf-8")
(export_dir / "missing-appid.json").write_text(json.dumps({"id": "sp-only"}), encoding="utf-8")
cache = self.module._load_resource_sp_cache_from_export(root)
self.assertEqual(cache, {})
if __name__ == "__main__":
unittest.main()