Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Script to list all delegated permissions and application permissions in Azure AD
<#
.SYNOPSIS
Lists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments).
.PARAMETER DelegatedPermissions
If set, will return delegated permissions. If neither this switch nor the ApplicationPermissions switch is set,
both application and delegated permissions will be returned.
.PARAMETER ApplicationPermissions
If set, will return application permissions. If neither this switch nor the DelegatedPermissions switch is set,
both application and delegated permissions will be returned.
.PARAMETER UserProperties
The list of properties of user objects to include in the output. Defaults to DisplayName only.
.PARAMETER ServicePrincipalProperties
The list of properties of service principals (i.e. apps) to include in the output. Defaults to DisplayName only.
.PARAMETER ShowProgress
Whether or not to display a progress bar when retrieving application permissions (which could take some time).
.PARAMETER PrecacheSize
The number of users to pre-load into a cache. For tenants with over a thousand users,
increasing this may improve performance of the script.
.EXAMPLE
PS C:\> .\Get-AzureADPSPermissions.ps1 | Export-Csv -Path "permissions.csv" -NoTypeInformation
Generates a CSV report of all permissions granted to all apps.
.EXAMPLE
PS C:\> .\Get-AzureADPSPermissions.ps1 -ApplicationPermissions -ShowProgress | Where-Object { $_.Permission -eq "Directory.Read.All" }
Get all apps which have application permissions for Directory.Read.All.
.EXAMPLE
PS C:\> .\Get-AzureADPSPermissions.ps1 -UserProperties @("DisplayName", "UserPrincipalName", "Mail") -ServicePrincipalProperties @("DisplayName", "AppId")
Gets all permissions granted to all apps and includes additional properties for users and service principals.
#>
[CmdletBinding()]
param(
[switch] $DelegatedPermissions,
[switch] $ApplicationPermissions,
[string[]] $UserProperties = @("DisplayName"),
[string[]] $ServicePrincipalProperties = @("DisplayName"),
[switch] $ShowProgress,
[int] $PrecacheSize = 999
)
# Get tenant details to test that Connect-AzureAD has been called
try {
$tenant_details = Get-AzureADTenantDetail
} catch {
throw "You must call Connect-AzureAD before running this script."
}
Write-Verbose ("TenantId: {0}, InitialDomain: {1}" -f `
$tenant_details.ObjectId, `
($tenant_details.VerifiedDomains | Where-Object { $_.Initial }).Name)
# An in-memory cache of objects by {object ID} andy by {object class, object ID}
$script:ObjectByObjectId = @{}
$script:ObjectByObjectClassId = @{}
# Function to add an object to the cache
function CacheObject ($Object) {
if ($Object) {
if (-not $script:ObjectByObjectClassId.ContainsKey($Object.ObjectType)) {
$script:ObjectByObjectClassId[$Object.ObjectType] = @{}
}
$script:ObjectByObjectClassId[$Object.ObjectType][$Object.ObjectId] = $Object
$script:ObjectByObjectId[$Object.ObjectId] = $Object
}
}
# Function to retrieve an object from the cache (if it's there), or from Azure AD (if not).
function GetObjectByObjectId ($ObjectId) {
if (-not $script:ObjectByObjectId.ContainsKey($ObjectId)) {
Write-Verbose ("Querying Azure AD for object '{0}'" -f $ObjectId)
try {
$object = Get-AzureADObjectByObjectId -ObjectId $ObjectId
CacheObject -Object $object
} catch {
Write-Verbose "Object not found."
}
}
return $script:ObjectByObjectId[$ObjectId]
}
# Function to retrieve all OAuth2PermissionGrants, either by directly listing them (-FastMode)
# or by iterating over all ServicePrincipal objects. The latter is required if there are more than
# 999 OAuth2PermissionGrants in the tenant, due to a bug in Azure AD.
function GetOAuth2PermissionGrants ([switch]$FastMode) {
if ($FastMode) {
Get-AzureADOAuth2PermissionGrant -All $true
} else {
$script:ObjectByObjectClassId['ServicePrincipal'].GetEnumerator() | ForEach-Object { $i = 0 } {
if ($ShowProgress) {
Write-Progress -Activity "Retrieving delegated permissions..." `
-Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) `
-PercentComplete (($i / $servicePrincipalCount) * 100)
}
$client = $_.Value
Get-AzureADServicePrincipalOAuth2PermissionGrant -ObjectId $client.ObjectId
}
}
}
$empty = @{} # Used later to avoid null checks
# Get all ServicePrincipal objects and add to the cache
Write-Verbose "Retrieving all ServicePrincipal objects..."
Get-AzureADServicePrincipal -All $true | ForEach-Object {
CacheObject -Object $_
}
$servicePrincipalCount = $script:ObjectByObjectClassId['ServicePrincipal'].Count
if ($DelegatedPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) {
# Get one page of User objects and add to the cache
Write-Verbose ("Retrieving up to {0} User objects..." -f $PrecacheSize)
Get-AzureADUser -Top $PrecacheSize | Where-Object {
CacheObject -Object $_
}
Write-Verbose "Testing for OAuth2PermissionGrants bug before querying..."
$fastQueryMode = $false
try {
# There's a bug in Azure AD Graph which does not allow for directly listing
# oauth2PermissionGrants if there are more than 999 of them. The following line will
# trigger this bug (if it still exists) and throw an exception.
$null = Get-AzureADOAuth2PermissionGrant -Top 999
$fastQueryMode = $true
} catch {
if ($_.Exception.Message -and $_.Exception.Message.StartsWith("Unexpected end when deserializing array.")) {
Write-Verbose ("Fast query for delegated permissions failed, using slow method...")
} else {
throw $_
}
}
# Get all existing OAuth2 permission grants, get the client, resource and scope details
Write-Verbose "Retrieving OAuth2PermissionGrants..."
GetOAuth2PermissionGrants -FastMode:$fastQueryMode | ForEach-Object {
$grant = $_
if ($grant.Scope) {
$grant.Scope.Split(" ") | Where-Object { $_ } | ForEach-Object {
$scope = $_
$grantDetails = [ordered]@{
"PermissionType" = "Delegated"
"ClientObjectId" = $grant.ClientId
"ResourceObjectId" = $grant.ResourceId
"Permission" = $scope
"ConsentType" = $grant.ConsentType
"PrincipalObjectId" = $grant.PrincipalId
}
# Add properties for client and resource service principals
if ($ServicePrincipalProperties.Count -gt 0) {
$client = GetObjectByObjectId -ObjectId $grant.ClientId
$resource = GetObjectByObjectId -ObjectId $grant.ResourceId
$insertAtClient = 2
$insertAtResource = 3
foreach ($propertyName in $ServicePrincipalProperties) {
$grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName)
$insertAtResource++
$grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName)
$insertAtResource ++
}
}
# Add properties for principal (will all be null if there's no principal)
if ($UserProperties.Count -gt 0) {
$principal = $empty
if ($grant.PrincipalId) {
$principal = GetObjectByObjectId -ObjectId $grant.PrincipalId
}
foreach ($propertyName in $UserProperties) {
$grantDetails["Principal$propertyName"] = $principal.$propertyName
}
}
New-Object PSObject -Property $grantDetails
}
}
}
}
if ($ApplicationPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) {
# Iterate over all ServicePrincipal objects and get app permissions
Write-Verbose "Retrieving AppRoleAssignments..."
$script:ObjectByObjectClassId['ServicePrincipal'].GetEnumerator() | ForEach-Object { $i = 0 } {
if ($ShowProgress) {
Write-Progress -Activity "Retrieving application permissions..." `
-Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) `
-PercentComplete (($i / $servicePrincipalCount) * 100)
}
$sp = $_.Value
Get-AzureADServiceAppRoleAssignedTo -ObjectId $sp.ObjectId -All $true `
| Where-Object { $_.PrincipalType -eq "ServicePrincipal" } | ForEach-Object {
$assignment = $_
$resource = GetObjectByObjectId -ObjectId $assignment.ResourceId
$appRole = $resource.AppRoles | Where-Object { $_.Id -eq $assignment.Id }
$grantDetails = [ordered]@{
"PermissionType" = "Application"
"ClientObjectId" = $assignment.PrincipalId
"ResourceObjectId" = $assignment.ResourceId
"Permission" = $appRole.Value
}
# Add properties for client and resource service principals
if ($ServicePrincipalProperties.Count -gt 0) {
$client = GetObjectByObjectId -ObjectId $assignment.PrincipalId
$insertAtClient = 2
$insertAtResource = 3
foreach ($propertyName in $ServicePrincipalProperties) {
$grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName)
$insertAtResource++
$grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName)
$insertAtResource ++
}
}
New-Object PSObject -Property $grantDetails
}
}
}
@evgaff-zz

This comment has been minimized.

Copy link

@evgaff-zz evgaff-zz commented Oct 25, 2018

Thanks for posting this. I've been trying to get this work by looking at each part of the script as I have to work with a huge directory.
Have you ever seen this? When running Get-AzureADOAuth2PermissionGrant -all $true it always errors out:
Get-AzureADOAuth2PermissionGrant : Unexpected end when deserializing array. Path 'value[1000]', line 1, position
359298.

I've updated to the latest AzureADPreview 2.0.2.5 and tried from multiple machines and accounts (into the same directory).

Interestingly, I can do -top 57, which returns exactly 1000 objects. When set to 58, it fails.

Any thoughts?

@shesha1

This comment has been minimized.

Copy link

@shesha1 shesha1 commented Nov 28, 2018

Thanks for this post.

the command get-AzureADOAuth2PermissionGrant is returning only finite number of results. If i run "get-AzureADOAuth2PermissionGrant -all $true" it is throwing the below error.
Get-AzureADOAuth2PermissionGrant : Unexpected end when deserializing array. Path 'value[1000]', line 1, position 366740.
At line:1 char:1

  • Get-AzureADOAuth2PermissionGrant -all $true
  •   + CategoryInfo          : NotSpecified: (:) [Get-AzureADOAuth2PermissionGrant], ApiException
      + FullyQualifiedErrorId : Microsoft.Open.AzureAD16.Client.ApiException,Microsoft.Open.AzureAD16.PowerShell.GetOAuth2PermissionGrants
    
    

Please let me know if you have any workaround for the same.

Is there any other command which provides the user scopes?

Thanks,
Shesh

@cwitjes

This comment has been minimized.

Copy link

@cwitjes cwitjes commented Dec 13, 2018

You can actually modify the script to read the permissions per Azure AD Service Prinicipal and using

Get-AzureADServicePrincipalOAuth2PermissionGrant -ObjectId $sp.ObjectId

instead of "Get-AzureADOAuth2PermissionGrant -all $true"

Snipset:

Get all registered applications

$AzSPAll = get-AzureADServicePrincipal -All $true

.
.

Get all existing OAuth2 permission grants, get the client, resource and scope details

Write-Verbose "Retrieving OAuth2PermissionGrants...per SP"

foreach($sp in $AzSPAll){
       
Get-AzureADServicePrincipalOAuth2PermissionGrant -ObjectId $sp.ObjectId | ForEach-Object {
    $grant = $_
    if ($grant.Scope) {

.
.
at least this worked for me.

hth,

Claus

@shesha1

This comment has been minimized.

Copy link

@shesha1 shesha1 commented Dec 14, 2018

Hi Claus,

Thanks for the solution. It worked like a charm!!

below is the format I am using..

$azurespnall = Get-AzureADServicePrincipal -all $true

Foreach ($spn in $azurespnall)
    {
    #$spn = "053f4ac8-8329-4ef8-993e-dca44c6150be"
    Get-AzureADServicePrincipalOAuth2PermissionGrant -ObjectId $spn.objectid | ForEach-Object { 
        
        $scope = $_

        $client = Get-AzureADObjectByObjectId -ObjectIds $spn.objectid
        $resource = Get-AzureADObjectByObjectId -ObjectIds $scope.Resourceid
        $principalDisplayname = ""
        if($scope.PrincipalID)
            {

            $principal = Get-AzureADObjectByObjectId -ObjectIds $scope.principalID

            $principalDisplayname = $principal.DisplayName
            }

           
           $scope.clientid
           $client.DisplayName
           $scope.Resourceid
           $resource.DisplayName
           $scope.Scope
           $principalDisplayname
           $principalDisplayname

     }

Regards,
Shesh

@psignoret

This comment has been minimized.

Copy link
Owner Author

@psignoret psignoret commented Dec 18, 2018

@evgaff @shesha1 There's currently a bug in Azure AD when you have more than 1000 OAuth2PermissionGrants (delegated permission grants) in the tenant. As @cwitjes rightly points out, a workaround available today is to query these from each ServicePrincipal object's. Unfortunately, this is orders of magnitude slower than the original approach.

I've updated the script to test for the bug, and if it is triggered (because it hasn't been fixed and your tenant has more than 1000 grants), then it will fall back to the per-object (slow) query.

@ryandriftingfat

This comment has been minimized.

Copy link

@ryandriftingfat ryandriftingfat commented Dec 16, 2019

Would it be possible to change this to export either samaccountname, UPN or mail attributes?

@psignoret

This comment has been minimized.

Copy link
Owner Author

@psignoret psignoret commented Dec 18, 2019

@ryandriftingfat I've updated the script to support -UserProperties and -ServicePrincipalProperties, so that you can list the properties you'd like to see. See the third example (at the top of the script) to see how to get UserPrincipalName and Mail. sAMAccountName is not available with Azure AD PowerShell, so you wouldn't be able to do that.

@ryandriftingfat

This comment has been minimized.

Copy link

@ryandriftingfat ryandriftingfat commented Dec 18, 2019

This is perfect, thank you so much.

@YassineSOUABNI

This comment has been minimized.

Copy link

@YassineSOUABNI YassineSOUABNI commented May 2, 2020

Hello,

Thank you so much for this script !
For my side, I am not facing any error but rather am trying to undestand the following interesting difference.

In a recent effort to extend the script's output so its result displays 2 additional columns representing
the description of the consented permission (UserConsentDescription, AdminConsentDescription),

If I compare some the description I get of the permissions, I see it is not the exact description string/text from the two below approaches.

Approach 1/ If I extend the script you provide by adding the following

as an additional member/column in the custom object being forged in the "Delegated permissions" data collection section
"UserConsentDescription" = $client.Oauth2Permissions.UserConsentDescription
as an additional member/column in the custom object being forged in the "Application permission" data collection section
"AdminConsentDescription" = $appRole.Description

Versus

Approach 2/ If I use the following code region

knowing that the variable $ResultOfTheOriginalScriptAsItIs simply contains the output of the script you provide

#region Get/Display the "AdminConsentDescription", "UserConsentDescription" for each unique "ResourceDisplayName"
$FullyQualifiedPermissions = $ResultOfTheOriginalScriptAsItIs | select -Unique ResourceDisplayName |%{
$resource = $_.ResourceDisplayName
(Get-AzureADServicePrincipal -filter "DisplayName eq '$resource'" -all $true).OAuth2Permissions |
select *, @{n="ResourceDisplayName";e={$resource}}
}
$FullyQualifiedPermissions | GM
$FullyQualifiedPermissions.count

$FullyQualifiedPermissions |
select ResourceDisplayName ,
AdminConsentDescription,
UserConsentDescription ,
AdminConsentDisplayName,
UserConsentDisplayName ,
Id ,
IsEnabled ,
Type ,
Value |
ogv

#endregion

The environment being quite large (50k consented mix of Delegated / Application permissions),
I am trying to understand what is the best approach to use

  • why is there this difference
    but also
  • why the first approach has a lot of what I am feeding into the column "UserConsentDescription" through $client.Oauth2Permissions.UserConsentDescription showing as empty

Again thanks !

@psignoret

This comment has been minimized.

Copy link
Owner Author

@psignoret psignoret commented May 2, 2020

$client.OAuth2Permissions is the collection of all delegated permissions defined on the client application's service principal. The. $client.OAuth2Permissions.UserConsentDescription will be the collection of all user consent descriptions of all delegated permissions defined on the client app's service principal. So, you have two issues there:

  1. The delegated permission is defined on the resource app, not on the client app.
  2. You need to select the specific delegated permission which was granted, not all of them.

So, to include the delegated permission display name or description, you could add the following ~L153:

# $scope is the delegated permission claim value, obtained from the OAuth2PermissionGrant
$scope = $_

# Get the service principal for the resource application
$resource = GetObjectByObjectId -ObjectId $grant.ResourceId

# Get the delegated permission (OAuth2Permission) with the same value as $scope
$delegatedPermission = $resource.OAuth2Permissions | Where-Object { $_.Value -ieq $scope }

# Include the delegated permission details in the output object
$grantDetails =  [ordered]@{
    "PermissionType" = "Delegated"
    "ClientObjectId" = $grant.ClientId
    "ResourceObjectId" = $grant.ResourceId
    "Permission" = $scope
    "ConsentType" = $grant.ConsentType
    "PrincipalObjectId" = $grant.PrincipalId
    "AdminConsentDisplayName" = $delegatedPermission.AdminConsentDisplayName
    "AdminConsentDescription" = $delegatedPermission.AdminConsentDescription
    "UserConsentDisplayName" = $delegatedPermission.UserConsentDisplayName
    "UserConsentDescription" = $delegatedPermission.UserConsentDescription
}
@r3-zbigniews

This comment has been minimized.

Copy link

@r3-zbigniews r3-zbigniews commented Jun 5, 2020

Hi @psignoret

What would be a best way to run this in Azure runbook and export in CSV ?
I tried to "plug in" Export-CSV switch at the end but it doesn't seem to export: ClientDisplayName, ResourceDisplayName and PrincipalDisplayName, any ideas?

$results = @(
....
)
$results | Export-Csv -Path "c:\tmp\permissions.csv" -Append -NoTypeInformation
@r3-zbigniews

This comment has been minimized.

Copy link

@r3-zbigniews r3-zbigniews commented Jun 18, 2020

Hi Philippe

Are you able to help with my previous post?
https://gist.github.com/psignoret/41793f8c6211d2df5051d77ca3728c09#gistcomment-3331284

@psignoret

This comment has been minimized.

Copy link
Owner Author

@psignoret psignoret commented Jun 22, 2020

@r3-zbigniews Can you include how you're calling the script?

@r3-zbigniews

This comment has been minimized.

Copy link

@r3-zbigniews r3-zbigniews commented Jun 22, 2020

@psignoret I'm using AzureRunAsConnection connection

$Conn = Get-AutomationConnection -Name AzureRunAsConnection
Connect-AzAccount -ServicePrincipal -Tenant $Conn.TenantID `
-ApplicationId $Conn.ApplicationID -CertificateThumbprint $Conn.CertificateThumbprint
Connect-AzureAD -TenantId "TENANTID" -ApplicationId "APPID" -CertificateThumbprint $Conn.CertificateThumbprint
@psignoret

This comment has been minimized.

Copy link
Owner Author

@psignoret psignoret commented Jun 22, 2020

@r3-zbigniews Can you share how you're calling this script?

@r3-zbigniews

This comment has been minimized.

Copy link

@r3-zbigniews r3-zbigniews commented Jun 23, 2020

$Conn = Get-AutomationConnection -Name AzureRunAsConnection
Connect-AzAccount -ServicePrincipal -Tenant $Conn.TenantID `
-ApplicationId $Conn.ApplicationID -CertificateThumbprint $Conn.CertificateThumbprint
Connect-AzureAD -TenantId TENANTID -ApplicationId APPID -CertificateThumbprint $Conn.CertificateThumbprint
$date = Get-Date -Format "ddMMyyyyTHHmmssZ" 
<#
.SYNOPSIS
    Lists delegated permissions (OAuth2PermissionGrants) and application permissions (AppRoleAssignments).
.PARAMETER DelegatedPermissions
    If set, will return delegated permissions. If neither this switch nor the ApplicationPermissions switch is set,
    both application and delegated permissions will be returned.
.PARAMETER ApplicationPermissions
    If set, will return application permissions. If neither this switch nor the DelegatedPermissions switch is set,
    both application and delegated permissions will be returned.
.PARAMETER UserProperties
    The list of properties of user objects to include in the output. Defaults to DisplayName only.
.PARAMETER ServicePrincipalProperties
    The list of properties of service principals (i.e. apps) to include in the output. Defaults to DisplayName only.
.PARAMETER ShowProgress
    Whether or not to display a progress bar when retrieving application permissions (which could take some time).
.PARAMETER PrecacheSize
    The number of users to pre-load into a cache. For tenants with over a thousand users,
    increasing this may improve performance of the script.
.EXAMPLE
    PS C:\> .\Get-AzureADPSPermissions.ps1 | Export-Csv -Path "permissions.csv" -NoTypeInformation
    Generates a CSV report of all permissions granted to all apps.
.EXAMPLE
    PS C:\> .\Get-AzureADPSPermissions.ps1 -ApplicationPermissions -ShowProgress | Where-Object { $_.Permission -eq "Directory.Read.All" }
    Get all apps which have application permissions for Directory.Read.All.
.EXAMPLE
    PS C:\> .\Get-AzureADPSPermissions.ps1 -UserProperties @("DisplayName", "UserPrincipalName", "Mail") -ServicePrincipalProperties @("DisplayName", "AppId")
    Gets all permissions granted to all apps and includes additional properties for users and service principals.
#>
$report = @(
# function something {
# [CmdletBinding()]
# param(
#     [switch] $DelegatedPermissions,

#     [switch] $ApplicationPermissions,

#     [string[]] $UserProperties = @("DisplayName"),

#     [string[]] $ServicePrincipalProperties = @("DisplayName"),

#     [switch] $ShowProgress,

#     [int] $PrecacheSize = 999
# )
# }
# Get tenant details to test that Connect-AzureAD has been called
try {
    $tenant_details = Get-AzureADTenantDetail
} catch {
    throw "You must call Connect-AzureAD before running this script."
}
Write-Verbose ("TenantId: {0}, InitialDomain: {1}" -f `
                $tenant_details.ObjectId, `
                ($tenant_details.VerifiedDomains | Where-Object { $_.Initial }).Name)

# An in-memory cache of objects by {object ID} andy by {object class, object ID}
$script:ObjectByObjectId = @{}
$script:ObjectByObjectClassId = @{}

# Function to add an object to the cache
function CacheObject ($Object) {
    if ($Object) {
        if (-not $script:ObjectByObjectClassId.ContainsKey($Object.ObjectType)) {
            $script:ObjectByObjectClassId[$Object.ObjectType] = @{}
        }
        $script:ObjectByObjectClassId[$Object.ObjectType][$Object.ObjectId] = $Object
        $script:ObjectByObjectId[$Object.ObjectId] = $Object
    }
}

# Function to retrieve an object from the cache (if it's there), or from Azure AD (if not).
function GetObjectByObjectId ($ObjectId) {
    if (-not $script:ObjectByObjectId.ContainsKey($ObjectId)) {
        Write-Verbose ("Querying Azure AD for object '{0}'" -f $ObjectId)
        try {
            $object = Get-AzureADObjectByObjectId -ObjectId $ObjectId
            CacheObject -Object $object
        } catch {
            Write-Verbose "Object not found."
        }
    }
    return $script:ObjectByObjectId[$ObjectId]
}

# Function to retrieve all OAuth2PermissionGrants, either by directly listing them (-FastMode)
# or by iterating over all ServicePrincipal objects. The latter is required if there are more than
# 999 OAuth2PermissionGrants in the tenant, due to a bug in Azure AD.
function GetOAuth2PermissionGrants ([switch]$FastMode) {
    if ($FastMode) {
        Get-AzureADOAuth2PermissionGrant -All $true
    } else {
        $script:ObjectByObjectClassId['ServicePrincipal'].GetEnumerator() | ForEach-Object { $i = 0 } {
            if ($ShowProgress) {
                Write-Progress -Activity "Retrieving delegated permissions..." `
                               -Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) `
                               -PercentComplete (($i / $servicePrincipalCount) * 100)
            }

            $client = $_.Value
            Get-AzureADServicePrincipalOAuth2PermissionGrant -ObjectId $client.ObjectId
        }
    }
}

$empty = @{} # Used later to avoid null checks

# Get all ServicePrincipal objects and add to the cache
Write-Verbose "Retrieving all ServicePrincipal objects..."
Get-AzureADServicePrincipal -All $true | ForEach-Object {
    CacheObject -Object $_
}
$servicePrincipalCount = $script:ObjectByObjectClassId['ServicePrincipal'].Count

if ($DelegatedPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) {

    # Get one page of User objects and add to the cache
    Write-Verbose ("Retrieving up to {0} User objects..." -f $PrecacheSize)
    Get-AzureADUser -Top $PrecacheSize | Where-Object {
        CacheObject -Object $_
    }

    Write-Verbose "Testing for OAuth2PermissionGrants bug before querying..."
    $fastQueryMode = $false
    try {
        # There's a bug in Azure AD Graph which does not allow for directly listing
        # oauth2PermissionGrants if there are more than 999 of them. The following line will
        # trigger this bug (if it still exists) and throw an exception.
        $null = Get-AzureADOAuth2PermissionGrant -Top 999
        $fastQueryMode = $true
    } catch {
        if ($_.Exception.Message -and $_.Exception.Message.StartsWith("Unexpected end when deserializing array.")) {
            Write-Verbose ("Fast query for delegated permissions failed, using slow method...")
        } else {
            throw $_
        }
    }

    # Get all existing OAuth2 permission grants, get the client, resource and scope details
    Write-Verbose "Retrieving OAuth2PermissionGrants..."
    GetOAuth2PermissionGrants -FastMode:$fastQueryMode | ForEach-Object {
        $grant = $_
        if ($grant.Scope) {
            $grant.Scope.Split(" ") | Where-Object { $_ } | ForEach-Object {

                $scope = $_

                $grantDetails =  [ordered]@{
                    "PermissionType" = "Delegated"
                    "ClientObjectId" = $grant.ClientId
                    #"ClientDisplayName" = $grant.ClientDisplayName
                    #"ResourceDisplayName" = $grant.ResourceDisplayName
                    "ResourceObjectId" = $grant.ResourceId
                    "Permission" = $scope
                    "ConsentType" = $grant.ConsentType
                    "PrincipalObjectId" = $grant.PrincipalId
                    #"PrincipalDisplayName" = $grant.PrincipalDisplayName
                }

                # Add properties for client and resource service principals
                if ($ServicePrincipalProperties.Count -gt 0) {

                    $client = GetObjectByObjectId -ObjectId $grant.ClientId
                    $resource = GetObjectByObjectId -ObjectId $grant.ResourceId

                    $insertAtClient = 2
                    $insertAtResource = 3
                    foreach ($propertyName in $ServicePrincipalProperties) {
                        $grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName)
                        $insertAtResource++
                        $grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName)
                        $insertAtResource ++
                    }
                }

                # Add properties for principal (will all be null if there's no principal)
                if ($UserProperties.Count -gt 0) {

                    $principal = $empty
                    if ($grant.PrincipalId) {
                        $principal = GetObjectByObjectId -ObjectId $grant.PrincipalId
                    }

                    foreach ($propertyName in $UserProperties) {
                        $grantDetails["Principal$propertyName"] = $principal.$propertyName
                    }
                }

                New-Object PSObject -Property $grantDetails
            }
        }
    }
}

if ($ApplicationPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) {

    # Iterate over all ServicePrincipal objects and get app permissions
    Write-Verbose "Retrieving AppRoleAssignments..."
    $script:ObjectByObjectClassId['ServicePrincipal'].GetEnumerator() | ForEach-Object { $i = 0 } {

        if ($ShowProgress) {
            Write-Progress -Activity "Retrieving application permissions..." `
                        -Status ("Checked {0}/{1} apps" -f $i++, $servicePrincipalCount) `
                        -PercentComplete (($i / $servicePrincipalCount) * 100)
        }

        $sp = $_.Value

        Get-AzureADServiceAppRoleAssignedTo -ObjectId $sp.ObjectId -All $true `
        | Where-Object { $_.PrincipalType -eq "ServicePrincipal" } | ForEach-Object {
            $assignment = $_

            $resource = GetObjectByObjectId -ObjectId $assignment.ResourceId
            $appRole = $resource.AppRoles | Where-Object { $_.Id -eq $assignment.Id }

            $grantDetails = [ordered]@{
                "PermissionType" = "Application"
                "ClientObjectId" = $assignment.PrincipalId
                "ResourceObjectId" = $assignment.ResourceId
                "Permission" = $appRole.Value
            }

            # Add properties for client and resource service principals
            if ($ServicePrincipalProperties.Count -gt 0) {

                $client = GetObjectByObjectId -ObjectId $assignment.PrincipalId

                $insertAtClient = 2
                $insertAtResource = 3
                foreach ($propertyName in $ServicePrincipalProperties) {
                    $grantDetails.Insert($insertAtClient++, "Client$propertyName", $client.$propertyName)
                    $insertAtResource++
                    $grantDetails.Insert($insertAtResource, "Resource$propertyName", $resource.$propertyName)
                    $insertAtResource ++
                }
            }

            New-Object PSObject -Property $grantDetails
        }
    }
}
)
$report | Export-CSV -nti -Path "$Env:temp\AADSPPermissions_$date.csv"
@r3-zbigniews

This comment has been minimized.

Copy link

@r3-zbigniews r3-zbigniews commented Jul 3, 2020

@psignoret what do you think?

@FredSp1

This comment has been minimized.

Copy link

@FredSp1 FredSp1 commented Jul 30, 2020

Thank you for the script.
How far back in time does this script go or how long are the logs kept in Azure? For a month? Or does it scan all apps in our tenant? Also is there any way to see who consented for the entire org? There seems to be very little info for an app in the Enterprise Applications list to assist with investigating a specific app.
Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.