Find Service Principals With High Risk Permissions and No Owners

In Microsoft Entra ID, one of the most critical security risks arises when a service principal:

  • ❌ Has high-risk permissions, AND
  • ❌ Has no assigned owners

This combination creates a dangerous scenario because:

  • No one is accountable for the identity
  • The identity has elevated access to sensitive resources
  • It can be misused or re-enabled without detection

👉 These are often referred to as orphaned high-privilege identities, and they should be prioritized during security audits.

This script helps administrators identify custom Entra service principals that have high-risk permissions and no owners, enabling immediate remediation.

🚀 Community Edition Released!

Try the M365Corner Microsoft 365 Reporting Tool — your DIY pack with 20+ out-of-the-box M365 reports for Users, Groups, and Teams.

The Script

                            
# Connect to Microsoft Graph
Connect-MgGraph -Scopes Application.Read.All, Directory.Read.All

Write-Host "Scanning CUSTOM SPs with HIGH-RISK permissions AND NO owners..." -ForegroundColor Cyan

# High-risk permissions list
$HighRiskPermissions = @(
    "Directory.ReadWrite.All",
    "User.ReadWrite.All",
    "Application.ReadWrite.All",
    "RoleManagement.ReadWrite.Directory",
    "Group.ReadWrite.All",
    "AppRoleAssignment.ReadWrite.All",
    "Directory.Read.All",
    "User.Read.All",
    "Group.Read.All"
)

# Get all service principals
$ServicePrincipals = Get-MgServicePrincipal -All -Property Id,DisplayName,AppId,Tags

# Cache for resource SPs
$ResourceSPCache = @{}

$Results = @()

foreach ($SP in $ServicePrincipals) {

    # -------------------------
    # Exclude Microsoft-managed SPs
    # -------------------------
    if (
        $SP.Tags -contains "WindowsAzureActiveDirectoryIntegratedApp" -or
        $SP.Tags -contains "MicrosoftApplication"
    ) {
        continue
    }

    # -------------------------
    # Check Owners FIRST (early exit optimization)
    # -------------------------
    $Owners = Get-MgServicePrincipalOwner -ServicePrincipalId $SP.Id -ErrorAction SilentlyContinue

    if ($Owners -and $Owners.Count -gt 0) {
        continue
    }

    # -------------------------
    # Get App Role Assignments
    # -------------------------
    $Assignments = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $SP.Id -All -ErrorAction SilentlyContinue

    if (-not $Assignments) {
        continue
    }

    $MatchedPermissions = @()

    foreach ($Assignment in $Assignments) {

        # Cache resource SP
        if (-not $ResourceSPCache.ContainsKey($Assignment.ResourceId)) {
            $ResourceSPCache[$Assignment.ResourceId] =
                Get-MgServicePrincipal -ServicePrincipalId $Assignment.ResourceId -Property AppRoles
        }

        $ResourceSP = $ResourceSPCache[$Assignment.ResourceId]

        $Role = $ResourceSP.AppRoles | Where-Object {
            $_.Id -eq $Assignment.AppRoleId
        }

        if ($Role -and $HighRiskPermissions -contains $Role.Value) {
            $MatchedPermissions += $Role.Value
        }
    }

    # Remove duplicates
    $MatchedPermissions = $MatchedPermissions | Sort-Object -Unique

    if ($MatchedPermissions.Count -gt 0) {

        # Console output (minimal)
        Write-Host "$($SP.DisplayName) | $($SP.AppId)" -ForegroundColor Red

        # Export object
        $Results += [PSCustomObject]@{
            DisplayName         = $SP.DisplayName
            ServicePrincipalId  = $SP.Id
            AppId               = $SP.AppId
            OwnerStatus         = "No Owner Assigned"
            HighRiskPermissions = ($MatchedPermissions -join ", ")
            PermissionCount     = $MatchedPermissions.Count
            RiskLevel           = "Critical"
            AppType             = "Custom / Non-Microsoft"
        }
    }
}

# Export results
$ExportPath = "D:\SP_HighRisk_NoOwners_Report.csv"

$Results | Export-Csv $ExportPath -NoTypeInformation

Write-Host "Critical risk report exported to $ExportPath" -ForegroundColor Cyan


How the Script Works

Step Description
Connect to Graph Authenticates using Application.Read.All and Directory.Read.All
Define High-Risk Permissions Stores a predefined list of sensitive permissions
Fetch Service Principals Retrieves all service principals
Exclude Microsoft Apps Skips Microsoft-managed service principals using tags
Check Owners Filters only service principals with no owners
Get App Role Assignments Retrieves assigned API permissions
Cache Resource SPs Improves performance by caching resource service principals
Resolve Permission Names Matches AppRoleId to permission names
Identify High-Risk Matches Filters only high-risk permissions
Build Report Stores details including permission count and risk level
Export Results Exports critical-risk identities to CSV

Further Enhancements

Enhancement Description
Include Sign-In Logs Identify whether the SP is actively used
Add Owner Remediation Automatically assign owners
Include Created Date Track how old the identity is
Risk Prioritization Rank based on number of high-risk permissions
Alert Integration Send alerts for critical findings

Frequently Asked Questions

Question Answer
Why is this scenario critical? High permissions + no owner = no accountability and high risk
What are high-risk permissions? Permissions that allow broad access or modification of directory data
Why exclude Microsoft apps? They are system-managed and generally safe
Can a service principal have multiple high-risk permissions? Yes, increasing the risk level
Should these be deleted immediately? No, validate usage before taking action

Admin Usecases

Use Case Description
Security Audit Identify critical-risk service principals
Governance Review Ensure ownership and permission control
Risk Detection Detect orphaned high-privilege identities
Compliance Reporting Meet regulatory and audit requirements
Incident Response Quickly identify potential attack vectors

Possible Errors & Solutions

Error Cause Solution
Insufficient privileges Missing Graph permissions Use Application.Read.All and Directory.Read.All
Cmdlet not recognized Graph module not installed Install using Install-Module Microsoft.Graph
Access token expired Session timeout Reconnect using Connect-MgGraph
Slow execution Large tenant or API calls Use caching (already implemented)

Conclusion

Service principals with high-risk permissions and no owners represent one of the most dangerous security gaps in Microsoft Entra ID. These identities combine elevated access with zero accountability, making them prime targets for misuse.

This Microsoft Graph PowerShell script provides a powerful way to identify such critical-risk identities and export them for immediate review. By regularly auditing and remediating these service principals, administrators can:

  • Enforce least privilege
  • Improve governance
  • Strengthen overall security posture

Incorporating this script into your security workflows ensures better visibility, control, and protection across your Entra environment.

Graph PowerShell Explorer Widget

20 Graph PowerShell cmdlets with easily accessible "working" examples.


Permission Required

Example:


                            


                            


                            

© Created and Maintained by LEARNIT WELL SOLUTIONS. All Rights Reserved.