Skip to content

Instantly share code, notes, and snippets.

@swhite-strath
Created December 19, 2024 16:54
Show Gist options
  • Save swhite-strath/7b37054a374ed476ecd6ea47c687c32b to your computer and use it in GitHub Desktop.
Save swhite-strath/7b37054a374ed476ecd6ea47c687c32b to your computer and use it in GitHub Desktop.
<#
.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 **for my script you are always exporting to CSV but you have to define the headers so if you add any more user properties you will need to update the headers**
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.
.NOTES
Original script done by @psignoret - https://gist.github.com/psignoret/41793f8c6211d2df5051d77ca3728c09
Adapted to MS Graph by @acap4z
Modfied to do away with lookup table and retrieve properties from the resource service principal @swhite-strath
#>
[CmdletBinding()]
param(
[switch] $DelegatedPermissions,
[switch] $ApplicationPermissions,
[string[]] $UserProperties = @("DisplayName"),
[string[]] $ServicePrincipalProperties = @("DisplayName"),
[switch] $ShowProgress,
[int] $PrecacheSize = 999
)
# Define the Application (Client) ID, Secret and CSV Location
$ApplicationClientId = '****' # Application (Client) ID
$ApplicationClientSecret = '****' # Application Secret Value
$TenantId = '****' # Tenant ID
$csvfile = "**CSV LOCATION**"
# Convert the Client Secret to a Secure String
$SecureClientSecret = ConvertTo-SecureString -String $ApplicationClientSecret -AsPlainText -Force
# Create a PSCredential Object Using the Client ID and Secure Client Secret
$ClientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $ApplicationClientId, $SecureClientSecret
# Connect to Microsoft Graph Using the Tenant ID and Client Secret Credential
Connect-MgGraph -TenantId $TenantId -ClientSecretCredential $ClientSecretCredential -NoWelcome
# Get tenant details to test that Connect-MgGraph has been called
$tenant_details = Get-MgOrganization
if (!($tenant_details)){
return
}
#Create CSV File
$headers = "PermissionType", "ClientName", "ClientObjectId", "ResourceObjectId", "ResourceDisplayName", "Permission", "ConsentType", "PrincipalObjectId", "UserConsentDescription", "AdminConsentDescription", "AdminConsentDisplayName", "UserConsentDisplayName","Type","PrincipalDisplayName"
Import-Module -Name SharePointPnPPowerShellOnline -DisableNameChecking
$psObject = New-Object psobject
foreach($header in $headers)
{
Add-Member -InputObject $psobject -MemberType noteproperty -Name $header -Value ""
}
$psObject | Export-Csv $csvfile -NoTypeInformation
Write-Verbose ("TenantId: {0}, InitialDomain: {1}" -f `
$tenant_details.Id, `
($tenant_details.VerifiedDomains | Where-Object { $_.IsInitial }).Name)
# An in-memory cache of objects by {object ID} andy by {object class, object ID}
$script:ObjectByObjectId = @{}
$script:CachedUserList = @{}
# Function to add an object to the cache
function CacheObject ($Object) {
if ($Object) {
$script:ObjectByObjectId[$Object.Id] = $Object
}
}
# Function to add a user to the cache
function CacheUser ($User) {
if ($User) {
$script:CachedUserList[$User.Id] = $User
}
}
# 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-MgDirectoryObjectById -Ids $ObjectId
CacheObject -Object $object
} catch {
Write-Verbose "Object not found."
}
}
return $script:ObjectByObjectId[$ObjectId]
}
# Function to retrieve an object from the cache (if it's there), or from Azure AD (if not).
function GetUserById ($UserId) {
if (-not $script:CachedUserList.ContainsKey($UserId)) {
Write-Verbose ("Querying Azure AD for user '{0}'" -f $UserId)
try {
$user = Get-MgUser -UserId $UserId
CacheUser -User $user
} catch {
Write-Verbose "User not found."
}
}
return $script:CachedUserList[$UserId]
}
# 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-MgOauth2PermissionGrant -All
} else {
$script:ObjectByObjectId.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-MgServicePrincipalOauth2PermissionGrant -ServicePrincipalId $client.Id
}
}
}
$empty = @{} # Used later to avoid null checks
# Get all ServicePrincipal objects and add to the cache
Write-Verbose "Retrieving all ServicePrincipal objects..."
Get-MgServicePrincipal -All | ForEach-Object {
CacheObject -Object $_
}
$servicePrincipalCount = $script:ObjectByObjectId.Count
Write-Verbose ("{0} ServicePrincipal objects have been cached" -f $servicePrincipalCount)
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-MgUser -Top $PrecacheSize | ForEach-Object {
CacheUser -User $_
}
Write-Verbose ("{0} User objects have been cached" -f $script:CachedUserList.Count)
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-MgOauth2PermissionGrant -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 = $_
$app =$script:ObjectByObjectId[$grant.ClientId]
$AppResourceID =$script:ObjectByObjectId[$grant.ResourceId]
$AppORoles = $AppResourceID | Select-Object -ExpandProperty Oauth2PermissionScopes
if ($grant.Scope) {
$grant.Scope.Split(" ") | Where-Object { $_ } | ForEach-Object {
$scope = $_
$appOPerm = $AppORoles | Where-Object { $_.Value -eq $scope }
$grantDetails = [ordered]@{
"PermissionType" = "Delegated"
"ClientName" = $app.DisplayName
"ClientObjectId" = $grant.ClientId
"ResourceObjectId" = $grant.ResourceId
"ResourceDisplayName" = $AppResourceID.DisplayName
"Permission" = $scope
"ConsentType" = $grant.ConsentType
"PrincipalObjectId" = $grant.PrincipalId
"UserConsentDescription" = $appOPerm.UserConsentDescription -replace ",", "" -replace "`r", "" -replace "`n", ""
"AdminConsentDescription" = $appOPerm.AdminConsentDescription -replace ",", "" -replace "`r", "" -replace "`n", ""
"AdminConsentDisplayName" = $appOPerm.AdminConsentDisplayName
"UserConsentDisplayName" = $appOPerm.UserConsentDisplayName
"Type" = $appOPerm.Type
}
# Add properties for principal (will all be null if there's no principal)
if ($UserProperties.Count -gt 0) {
$principal = $empty
if ($grant.PrincipalId) {
$principal = GetUserById($grant.PrincipalId)
}
foreach ($propertyName in $UserProperties) {
$grantDetails["Principal$propertyName"] = $principal.$propertyName
}
}
#New-Object PSObject -Property $grantDetails
$ExportObj = New-Object PSObject -Property $grantDetails
Export-Csv -LiteralPath $csvfile -inputobject $ExportObj -append -Force -NoTypeInformation
}
}
}
}
if ($ApplicationPermissions -or (-not ($DelegatedPermissions -or $ApplicationPermissions))) {
# Iterate over all ServicePrincipal objects and get app permissions
Write-Verbose "Retrieving AppRoleAssignments..."
$script:ObjectByObjectId.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-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id -All `
| Where-Object { $_.PrincipalType -eq "ServicePrincipal" } | ForEach-Object {
$assignment = $_
$app =$script:ObjectByObjectId[$sp.Id]
$Resourceapp = $script:ObjectByObjectId[$assignment.ResourceId]
$AppRoles = $Resourceapp | Select-Object -ExpandProperty AppRoles
$appPerm = $appRoles | Where-Object { $_.Id -eq $assignment.AppRoleId }
$grantDetails = [ordered]@{
"PermissionType" = "Application"
"ClientName" = $app.DisplayName
"ClientObjectId" = $assignment.PrincipalId
"ResourceObjectId" = $assignment.ResourceId
"ResourceDisplayName" = $assignment.ResourceDisplayName
"Permission" = $appPerm.Value
"ConsentType" = "AllPrincipals"
"PrincipalObjectId" = ""
"UserConsentDescription" = ""
"AdminConsentDescription" = $appPerm.Description -replace ",", "" -replace "`r", "" -replace "`n", ""
"AdminConsentDisplayName" = $appPerm.DisplayName
"UserConsentDisplayName" = ""
"Type" = ""
}
#New-Object PSObject -Property $grantDetails
$ExportObj = New-Object PSObject -Property $grantDetails
Export-Csv -LiteralPath $csvfile -inputobject $ExportObj -append -Force -NoTypeInformation
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment