Public/Discovery/Get-ServicePrincipalsPermission.ps1

function Get-ServicePrincipalsPermission {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [Alias('AppId','ApplicationId')]
        # [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 {
            # Resolve input that could be either service principal objectId or applicationId
            $resolvedSp = $null
            $resolvedSpId = $null

            # First try treating input as service principal objectId
            try {
                $resolvedSp = Invoke-MsGraph -relativeUrl "servicePrincipals/$servicePrincipalId" -NoBatch -ErrorAction Stop
                $resolvedSpId = $resolvedSp.id
                Write-Verbose "Resolved service principal by objectId: $resolvedSpId"
            }
            catch {
                Write-Verbose "Direct servicePrincipalId lookup failed, attempting appId lookup"
            }

            if (-not $resolvedSp) {
                $spByAppId = Invoke-MsGraph -relativeUrl "servicePrincipals?`$filter=appId eq '$servicePrincipalId'" -NoBatch
                if ($spByAppId.value -and $spByAppId.value.Count -gt 0) {
                    $resolvedSp = $spByAppId.value[0]
                    $resolvedSpId = $resolvedSp.id
                    Write-Verbose "Resolved service principal by applicationId to objectId: $resolvedSpId"
                }
            }

            if (-not $resolvedSpId) {
                throw "Unable to resolve service principal from identifier '$servicePrincipalId'"
            }

            Write-Verbose "Creating batch requests for service principal $resolvedSpId"
            
            # 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/$resolvedSpId"
                })
            
            # Request 2: Get app role assignments
            $batchRequests.Add(@{
                    id     = "appRoleAssignments"
                    method = "GET"
                    url    = "/servicePrincipals/$resolvedSpId/appRoleAssignments"
                })
            
            # Request 3: Get delegated permissions
            $batchRequests.Add(@{
                    id     = "delegatedPermissions"
                    method = "GET"
                    url    = "/oauth2PermissionGrants?`$filter=clientId eq '$resolvedSpId'"
                })
            
            # Request 4: Get app roles assigned to others
            $batchRequests.Add(@{
                    id     = "appRoleAssignedTo"
                    method = "GET"
                    url    = "/servicePrincipals/$resolvedSpId/appRoleAssignedTo"
                })
            
            # Request 5: Get directory roles and memberships
            $batchRequests.Add(@{
                    id     = "memberOf"
                    method = "GET"
                    url    = "/servicePrincipals/$resolvedSpId/transitiveMemberOf"
                })
            
            # Request 6: Get owned objects
            $batchRequests.Add(@{
                    id     = "ownedObjects"
                    method = "GET"
                    url    = "/servicePrincipals/$resolvedSpId/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
                }
            }

            $appPermissionsSummary = $appPermissions | Group-Object 'Resource DisplayName' | ForEach-Object {
                $permissionList = $_.Group | ForEach-Object {
                    if ($_."Permission Name" -and $_."Permission Name" -ne "Unknown") {
                        $_."Permission Name"
                    }
                    else {
                        $_."PermissionId"
                    }
                }

                "$($_.Name): $($permissionList -join ', ')"
            }
            
            $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           = $appPermissionsSummary
                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
Analyzes service principal permissions, roles, and relationships.
 
.DESCRIPTION
Analyzes service principal permissions, roles, and relationships. This function performs comprehensive security analysis of a service principal including its app roles, directory permissions, API permissions, and relationships to other resources. Useful for assessing service principal attack surface.
 
.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.
 
.LINK
MITRE ATT&CK Tactic: TA0007 - Discovery
https://attack.mitre.org/tactics/TA0007/
 
.LINK
MITRE ATT&CK Technique: T1087.004 - Account Discovery: Cloud Account
https://attack.mitre.org/techniques/T1087/004/
#>

}