prowler-cloud/prowler

[New Check]: Service principals with privileged Entra directory roles must not have owners

Open

#11070 opened on May 6, 2026

View on GitHub
 (3 comments) (0 reactions) (0 assignees)Python (8,957 stars) (1,322 forks)batch import
feature-requestgood first issuenew-checkprovider/m365

Description

Existing check search

  • I have searched existing issues, Prowler Hub, and the public roadmap, and this check does not already exist.

Provider

Microsoft 365

New provider name

No response

Service or product area

entra

Suggested check name

entra_service_principal_privileged_role_no_owners

Context and goal

  • Security condition to validate: No service principal that holds a privileged Microsoft Entra directory role (Global Administrator, Application Administrator, Privileged Role Administrator, Privileged Authentication Administrator, Cloud Application Administrator, Hybrid Identity Administrator, etc.) has any owner — neither on the service principal itself nor on its parent application registration.
  • Why it matters: An owner of a service principal or app registration can rotate its credentials, add new ones (passwordCredentials / keyCredentials / federatedIdentityCredentials), or transfer ownership. If the underlying app holds a privileged directory role, the owner effectively becomes a back-door path into that role: they can mint a fresh secret, sign in as the app, and inherit Global Admin (or equivalent) privileges — all outside Privileged Identity Management approval flows and often outside Conditional Access scrutiny aimed at user accounts. Microsoft documents this as a known privilege-escalation pattern; the recommendation is that any service principal with a privileged role be ownerless and managed exclusively via PIM-eligible role assignments and break-glass controls.
  • Resource involved: Microsoft Entra service principals and applications, plus their roleAssignments against the unifiedRoleManagement API.

Expected behavior

  • Resource or scope to evaluate:
    1. List directory role assignments via GET /roleManagement/directory/roleAssignments?$expand=principal and keep entries whose principal.@odata.type = #microsoft.graph.servicePrincipal.
    2. Restrict to privileged roles. Use a hardcoded constant of well-known privileged role template IDs (Global Administrator 62e90394-69f5-4237-9190-012177145e10, Application Administrator 9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3, Privileged Role Administrator e8611ab8-c189-46e8-94e1-60213ab1f814, Privileged Authentication Administrator 7be44c8a-adaf-4e2a-84d6-ab2649e08a13, Cloud Application Administrator 158c047a-c907-4556-b7ef-446551a6b5f7, Hybrid Identity Administrator 8ac3fc64-6eca-42ea-9e69-59f4c7b60eb2, Authentication Administrator c4e39bd9-1100-46d3-8c65-fb160da0071f, Conditional Access Administrator b1be1c3e-b65d-4f19-8427-f6fa0d97feb9, Security Administrator 194ae4cb-b126-40b2-bd5b-6091b380977d, User Administrator fe930be7-5e62-47db-91af-98c3a49a38b1).
    3. For each matching service principal, fetch its owners (GET /servicePrincipals/{id}/owners) and the owners of its parent application (GET /applications/{appObjectId}/owners) when an in-tenant application exists for the same appId.
  • PASS when: every privileged-role service principal has zero owners on both the service principal and the parent application.
  • FAIL when: at least one privileged-role service principal has at least one owner on either the service principal or the parent application. The finding should report the service principal displayName, appId, the privileged role(s) it holds, and the owner principal IDs.
  • MANUAL when: the calling identity does not have permission to enumerate role assignments or owners (Graph 403). Mark MANUAL with a status message asking the operator to grant the required scopes.
  • Exclusions / edge cases:
    • Skip Microsoft first-party service principals (those whose appOwnerOrganizationId matches the well-known Microsoft tenant IDs). They are managed by Microsoft and their owner model is out of customer control.
    • Disabled service principals (accountEnabled = false) are out of scope.
    • The hardcoded privileged-role list should live as a module-level constant so it is easy to extend if Microsoft introduces new privileged roles.

References

Suggested severity

High

Additional implementation notes

  • Existing patterns to follow: Reuse the service-principal enumeration pattern from entra_app_registration_no_unused_privileged_permissions (prowler/providers/m365/services/entra/). Extend entra_service.py so the ServicePrincipal and Application models expose an owners collection (a list of principal IDs is enough for the check).
  • Permissions / scopes: No additional permissions beyond Prowler's M365 baseline. Directory.Read.All already grants reads against roleManagement/directory/roleAssignments, servicePrincipals/{id}/owners, and applications/{id}/owners.
  • PowerShell is NOT needed; the check uses Microsoft Graph v1.0 only.
  • Privileged roles constant: keep the list explicit and short. Do NOT pull every role tagged as isPrivileged = true from /roleManagement/directory/roleDefinitions at runtime — that surface drifts as Microsoft adds roles, and a check that quietly broadens scope is harder to reason about in noise/triage. A static, reviewable list is the right trade-off here.
  • Related check (do NOT duplicate, complementary): entra_app_registration_no_unused_privileged_permissions audits unused API permissions (Microsoft Graph delegated/application permissions). This new check audits directory role assignments, which is a different surface (Entra RBAC vs Graph permissions).
  • Metadata: follow the M365 metadata schema used by sibling checks under entra/.

Contributor guide