Public/Discovery/Get-ServicePrincipalsPermission.ps1

function Get-ServicePrincipalsPermission {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        # [ValidatePattern('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', ErrorMessage = "It does not match expected GUID pattern")]
        [string]$servicePrincipalId
    )

    begin {
        Write-Verbose "Starting function $($MyInvocation.MyCommand.Name)"
        # Only invoke the authentication if we need to (tokens expired or not present)
        if (-not $script:graphHeader -or 
            -not $script:SessionVariables -or 
            -not $script:SessionVariables.accessToken -or 
            ($script:SessionVariables.ExpiresOn -and $script:SessionVariables.ExpiresOn - [datetime]::UtcNow.AddMinutes(-5) -le 0)) {
                
            Write-Verbose "Authentication needed - Initializing Graph API access"
            $MyInvocation.MyCommand.Name | Invoke-BlackCat -ResourceTypeName 'MSGraph'
        }
        else {
            Write-Verbose "Using existing authentication token - valid until $($script:SessionVariables.ExpiresOn)"
        }
    }

    process {
        try {
            Write-Verbose "Creating batch requests for service principal $servicePrincipalId"
            
            # Create batch requests for all needed data
            $batchRequests = [System.Collections.Generic.List[hashtable]]::new()
            
            # Request 1: Get service principal details
            $batchRequests.Add(@{
                    id     = "spDetails"
                    method = "GET"
                    url    = "/servicePrincipals/$servicePrincipalId"
                })
            
            # Request 2: Get app role assignments
            $batchRequests.Add(@{
                    id     = "appRoleAssignments"
                    method = "GET"
                    url    = "/servicePrincipals/$servicePrincipalId/appRoleAssignments"
                })
            
            # Request 3: Get delegated permissions
            $batchRequests.Add(@{
                    id     = "delegatedPermissions"
                    method = "GET"
                    url    = "/oauth2PermissionGrants?`$filter=clientId eq '$servicePrincipalId'"
                })
            
            # Request 4: Get app roles assigned to others
            $batchRequests.Add(@{
                    id     = "appRoleAssignedTo"
                    method = "GET"
                    url    = "/servicePrincipals/$servicePrincipalId/appRoleAssignedTo"
                })
            
            # Request 5: Get directory roles and memberships
            $batchRequests.Add(@{
                    id     = "memberOf"
                    method = "GET"
                    url    = "/servicePrincipals/$servicePrincipalId/transitiveMemberOf"
                })
            
            # Request 6: Get owned objects
            $batchRequests.Add(@{
                    id     = "ownedObjects"
                    method = "GET"
                    url    = "/servicePrincipals/$servicePrincipalId/ownedObjects"
                })
            
            # Execute all requests in a single batch
            Write-Verbose "Executing batch request with $($batchRequests.Count) items"
            $batchResults = Invoke-MsGraph -BatchRequests $batchRequests
            
            # Extract results from batch response
            $spDetails = $batchResults["spDetails"].Data
            $appRoleAssignments = $batchResults["appRoleAssignments"].Data.value
            $delegatedPermissions = $batchResults["delegatedPermissions"].Data.value
            $appRoleAssignedTo = $batchResults["appRoleAssignedTo"].Data.value
            $memberOf = $batchResults["memberOf"].Data.value
            $ownedObjects = $batchResults["ownedObjects"].Data.value
            
            Write-Verbose "Successfully retrieved all service principal data in a single batch request"
            
            # Extract useful data for summary
            $appPermissions = $appRoleAssignments | ForEach-Object {
                # Try to resolve permission name from appRoleId
                $currentAppRoleId = $_.appRoleId
                $permissionName = "Unknown"
                if ($script:SessionVariables -and $script:SessionVariables.appRoleIds) {
                    $permissionObj = $script:SessionVariables.appRoleIds | Where-Object { $_.appRoleId -eq $currentAppRoleId }
                    if ($permissionObj) {
                        $permissionName = $permissionObj.Permission
                    }
                    else {
                        # Try to call Get-AppRolePermission directly
                        try {
                            $roleInfo = Get-AppRolePermission -appRoleId $currentAppRoleId -ErrorAction SilentlyContinue
                            if ($roleInfo -and $roleInfo.Permission) {
                                $permissionName = $roleInfo.Permission
                            }
                        }
                        catch {
                            # Silently continue if Get-AppRolePermission fails
                        }
                    }
                }

                [PSCustomObject]@{
                    'Resource DisplayName' = $_.resourceDisplayName
                    'PermissionId'         = $_.appRoleId
                    'Permission Name'      = $permissionName
                }
            }
            
            $delegatedPerms = $delegatedPermissions | ForEach-Object {
                [PSCustomObject]@{
                    ResourceId = $_.resourceId
                    Scopes     = $_.scope -split ' '
                }
            }
            
            # Extract owned objects with type information
            $ownedObjectsInfo = $ownedObjects | ForEach-Object {
                $type = $_.'@odata.type' -replace '#microsoft\.graph\.'
                [PSCustomObject]@{
                    DisplayName = $_.displayName
                    ObjectId    = $_.id
                    Type        = $type
                }
            }
            
            # Create summarized result object
            $result = [PSCustomObject]@{
                DisplayName              = $spDetails.displayName
                ObjectId                 = $spDetails.id
                AppId                    = $spDetails.appId
                ServicePrincipalType     = $spDetails.servicePrincipalType
                AccountEnabled           = $spDetails.accountEnabled
                AppRoles                 = $spDetails.appRoles.displayName
                GroupMemberships         = ($memberOf | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.group' }).displayName
                DirectoryRoles           = ($memberOf | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.directoryRole' }).displayName
                AppPermissions           = $appPermissions
                DelegatedPermissions     = $delegatedPerms
                OwnedObjects             = $ownedObjectsInfo
                IsPrivileged             = $false
            }
            
            # Check if the service principal has privileged roles
            $privilegedRoles = @('Global Administrator', 'Privileged Role Administrator', 'Application Administrator', 
                'Cloud Application Administrator', 'Hybrid Identity Administrator', 'Directory Synchronization Accounts')
            foreach ($role in $result.DirectoryRoles) {
                if ($role -in $privilegedRoles) {
                    $result.IsPrivileged = $true
                    break
                }
            }
            
            return $result
        }
        catch {
            Write-Message -FunctionName $($MyInvocation.MyCommand.Name) -Message $($_.Exception.Message) -Severity 'Error'
        }
    }
    <#
.SYNOPSIS
Conducts a comprehensive security analysis of a service principal, including its permissions, roles, and relationships.
 
.DESCRIPTION
The Get-ServicePrincipalsPermission function performs an in-depth analysis of an Azure service principal's
security posture and permissions. It provides a centralized view of critical information including:
 
- Core identity details (DisplayName, ObjectId/ServicePrincipalId, AppId)
- Application permissions with both IDs and human-readable names
- Delegated OAuth2 permissions and their scopes
- Group memberships and directory role assignments
- Owned objects with their types and names
- Security indicators like privileged role assignments
- Exposure assessment through permissions assigned to others
 
.PARAMETER servicePrincipalId
The unique identifier (GUID) of the service principal to analyze. This can be passed from the pipeline.
 
.EXAMPLE
Get-ServicePrincipalsPermission -servicePrincipalId "12345678-1234-1234-1234-1234567890ab"
 
Retrieves comprehensive security information about the specified service principal, including all permissions,
roles, and relationships.
 
.EXAMPLE
Get-ServicePrincipalsPermission -servicePrincipalId "12345678-1234-1234-1234-1234567890ab" -Verbose
 
Performs detailed analysis with progress information shown for each API call, useful for troubleshooting
or understanding the data collection process.
 
.EXAMPLE
Get-ServicePrincipalsPermission -servicePrincipalId "12345678-1234-1234-1234-1234567890ab" | Select-Object -ExpandProperty AppPermissions
 
Extracts just the application permissions assigned to the service principal, showing resource names,
permission IDs and human-readable permission names.
 
.OUTPUTS
[PSCustomObject]
Returns a structured object containing detailed security information about the service principal:
- Basic details: DisplayName, ServicePrincipalId/ObjectId, AppId, ServicePrincipalType
- Status: AccountEnabled
- Permissions: AppPermissions (with both IDs and names), DelegatedPermissions
- Relationships: GroupMemberships, DirectoryRoles, OwnedObjects (with types)
- Security indicators: IsPrivileged, AssignedPermissionsCount, OwnedObjectsCount
 
.NOTES
- Uses Microsoft Graph batch API to retrieve all data in a single HTTP request, significantly improving performance.
- IsPrivileged flag specifically checks for high-risk directory roles like Global Administrator.
- The function attempts to resolve permission names from IDs using session variables or the Get-AppRolePermission function.
- Aligned with the output format of other BlackCat reconnaissance functions for consistent analysis.
- Optimized for large environments with many service principals and complex permission structures.
#>

}