Public/Entra/Application/Get-MgApplicationAssignment.ps1
|
<#
.SYNOPSIS Retrieves all Entra ID applications and their assignment types. .DESCRIPTION This function returns a list of all Entra ID applications with their assignment information, identifying whether they are assigned to all users or have specific assignments. If no assignments exist, it indicates whether the application is available to "all users". Give information about assigned users, groups, or service principals and if the group is protected/static/dynamic. .PARAMETER ApplicationId (Optional) One or more Application IDs (AppId) to filter the results. If not provided, all applications will be processed. .PARAMETER ObjectID (Optional) ObjectID (GUID) of a single service principal to target. Cannot be combined with -ApplicationId or -DisplayName. .PARAMETER DisplayName (Optional) Display name of a single service principal to target (exact match, with fallback on trimmed comparison). Cannot be combined with -ApplicationId or -ObjectID. .PARAMETER AllApplications (Optional) If specified, retrieves all service principals regardless of type. By default, only Enterprise Applications (tagged 'WindowsAzureActiveDirectoryIntegratedApp') are returned. .PARAMETER AssignmentNotEnforced (Optional) If specified, only applications where AppRoleAssignmentRequired is $false (open to all users, no assignment needed) will be returned. .PARAMETER AssignmentEmpty (Optional) If specified, only applications with no specific user/group/service principal assignments will be returned. Note: these apps may still be accessible to all users if AppRoleAssignmentRequired is $false. .PARAMETER ExportToExcel (Optional) If specified, exports the results to an Excel file in the user's profile directory. .PARAMETER DisableParallel (Optional) Forces sequential processing. By default, on PowerShell 7+ the function analyzes applications in parallel (ForEach-Object -Parallel) to speed up processing; on PowerShell 5.1 it always runs sequentially. .PARAMETER ThrottleLimit (Optional) Maximum number of concurrent runspaces when running in parallel. Default is 5. Keep this value moderate to avoid Microsoft Graph throttling (HTTP 429). .EXAMPLE Get-MgApplicationAssignment Retrieves all applications and their assignment types. .EXAMPLE Get-MgApplicationAssignment -DisableParallel Forces sequential processing even on PowerShell 7+ (useful for debugging or to avoid concurrent Graph calls). .EXAMPLE Get-MgApplicationAssignment -ApplicationId "xxx", "yyy" Retrieves assignment types for the specified application IDs. .EXAMPLE Get-MgApplicationAssignment -ObjectID '11111111-2222-3333-4444-555555555555' Retrieves assignment types for the service principal matching this ObjectID. .EXAMPLE Get-MgApplicationAssignment -DisplayName 'My SAML App' Retrieves assignment types for the service principal matching this DisplayName. .EXAMPLE Get-MgApplicationAssignment -AssignmentEmpty Retrieves only applications with no specific user/group/service principal assignments. .EXAMPLE Get-MgApplicationAssignment -ExportToExcel Gets all applications and exports them to an Excel file .LINK https://ps365.clidsys.com/docs/commands/Get-MgApplicationAssignment #> function Get-MgApplicationAssignment { [CmdletBinding(DefaultParameterSetName = 'All')] param( [Parameter(Mandatory = $false, Position = 0, ParameterSetName = 'ByApplicationId')] [String[]]$ApplicationId, [Parameter(Mandatory = $false, ParameterSetName = 'ByObjectId')] [string]$ObjectID, [Parameter(Mandatory = $false, ParameterSetName = 'ByDisplayName')] [string]$DisplayName, [Parameter(Mandatory = $false, ParameterSetName = 'All')] [switch]$AllApplications, [Parameter(Mandatory = $false)] [switch]$AssignmentNotEnforced, [Parameter(Mandatory = $false)] [switch]$AssignmentEmpty, [Parameter(Mandatory = $false)] [switch]$ExportToExcel, [Parameter(Mandatory = $false)] [switch]$DisableParallel, [Parameter(Mandatory = $false)] [ValidateRange(1, 20)] [int]$ThrottleLimit = 5 ) # Run in parallel by default on PowerShell 7+ (ForEach-Object -Parallel); always sequential on PowerShell 5.1. $useParallel = ($PSVersionTable.PSVersion.Major -ge 7) -and -not $DisableParallel $spProperty = 'Id,AppId,DisplayName,AppRoles,AppRoleAssignmentRequired,PublisherName,Tags,ServicePrincipalType,CreatedDateTime' # Get all service principals (enterprise applications) switch ($PSCmdlet.ParameterSetName) { 'ByApplicationId' { $servicePrincipals = @() foreach ($appId in $ApplicationId) { try { $sp = Get-MgServicePrincipal -Filter "AppId eq '$appId'" -Property $spProperty -ErrorAction Stop if ($sp) { $servicePrincipals += $sp } } catch { Write-Warning "Could not find application with ID: $appId" } } } 'ByObjectId' { try { $servicePrincipals = @(Get-MgServicePrincipal -ServicePrincipalId $ObjectID -Property $spProperty -ErrorAction Stop) } catch { Write-Warning "Could not find service principal with ObjectID: $ObjectID" return } } 'ByDisplayName' { $escaped = $DisplayName -replace "'", "''" $filter = "DisplayName eq '$escaped'" Write-Verbose "Filtering service principals with: $filter" $servicePrincipals = @(Get-MgServicePrincipal -Filter $filter -All -Property $spProperty) # Fallback: trimmed display name comparison if no exact match if (-not $servicePrincipals) { Write-Verbose "No exact match for '$DisplayName'. Searching with startswith and trimmed client-side comparison." $startsWithFilter = "startswith(DisplayName, '$escaped')" $candidates = Get-MgServicePrincipal -Filter $startsWithFilter -All -Property $spProperty $servicePrincipals = @($candidates | Where-Object { $_.DisplayName.Trim() -eq $DisplayName.Trim() }) } if (-not $servicePrincipals) { Write-Warning "No service principal found matching DisplayName '$DisplayName'." return } } default { if ($AllApplications) { $servicePrincipals = Get-MgServicePrincipal -All -Property $spProperty } else { # Default: Enterprise Applications only (tagged 'WindowsAzureActiveDirectoryIntegratedApp') $servicePrincipals = Get-MgServicePrincipal -All -Property $spProperty ` -Filter "tags/Any(x: x eq 'WindowsAzureActiveDirectoryIntegratedApp')" } } } # Initialize results array [System.Collections.Generic.List[PSCustomObject]]$applicationAssignmentsArray = @() Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] Starting analysis of $($servicePrincipals.Count) applications..." -ForegroundColor Cyan # Per-service-principal processing. Emits one object per assignment (or a fallback object). # Shared by the sequential and parallel paths. $processSp = { param($sp, $Prefix = '') Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] ${Prefix}Analyzing application: $($sp.DisplayName)" -ForegroundColor Cyan # Get app role assignments for this service principal $appRoleAssignments = Get-MgServicePrincipalAppRoleAssignedTo -ServicePrincipalId $sp.Id if ($appRoleAssignments.Count -gt 0) { # Process each assignment individually foreach ($assignment in $appRoleAssignments) { # Initialize assignment properties in logical order for readability $assignmentProps = [ordered]@{ # Application info (most important first) ApplicationName = $sp.DisplayName ApplicationType = if ($sp.Tags -contains 'WindowsAzureActiveDirectoryIntegratedApp') { 'Enterprise Application' } else { $sp.ServicePrincipalType } AssignmentType = '' PrincipalType = '' IsRoleAssignableGroup = $null # Principal details (User, Group, or Service Principal) UserName = $null UserPrincipalName = $null GroupName = $null GroupType = $null GroupMembershipType = $null ServicePrincipalName = $null ServicePrincipalType = $null PrincipalDisplayName = $null # Role and permission details AppRoleValue = ($sp.AppRoles | Where-Object { $_.Id -eq $assignment.AppRoleId }).Value AppRoleId = $assignment.AppRoleId AppRoleAssignmentRequired = $sp.AppRoleAssignmentRequired # Technical IDs (less important, at the end) ApplicationId = $sp.AppId ServicePrincipalId = $sp.Id UserId = $null GroupId = $null AssignedServicePrincipalId = $null PrincipalId = $null IsAssignableToRole = $null ServicePrincipalAppId = $null # Metadata (at the end) ApplicationPublisher = $sp.PublisherName CreatedDate = $assignment.CreatedDateTime } if ($assignment.PrincipalType -eq 'User') { $assignmentProps.AssignmentType = 'User Assignment' $assignmentProps.PrincipalType = 'User' try { $user = Get-MgUser -UserId $assignment.PrincipalId -ErrorAction SilentlyContinue if ($user) { Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] -> User found: $($user.DisplayName)" -ForegroundColor Green $assignmentProps.UserName = $user.DisplayName $assignmentProps.UserPrincipalName = $user.UserPrincipalName $assignmentProps.UserId = $user.Id } else { $assignmentProps.AssignmentType = 'User Assignment (Not Found)' $assignmentProps.UserName = "User ID: $($assignment.PrincipalId)" $assignmentProps.UserPrincipalName = 'Unknown' $assignmentProps.UserId = $assignment.PrincipalId } } catch { Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] -> Error retrieving user $($assignment.PrincipalId): $($_.Exception.Message)" -ForegroundColor Red $assignmentProps.AssignmentType = 'User Assignment (Error)' $assignmentProps.UserName = "User ID: $($assignment.PrincipalId)" $assignmentProps.UserPrincipalName = 'Unknown' $assignmentProps.UserId = $assignment.PrincipalId } } elseif ($assignment.PrincipalType -eq 'Group') { $assignmentProps.AssignmentType = 'Group Assignment' $assignmentProps.PrincipalType = 'Group' try { $group = Get-MgGroup -GroupId $assignment.PrincipalId -ErrorAction SilentlyContinue if ($group) { $protectedStatus = if ($null -ne $group.IsAssignableToRole) { $group.IsAssignableToRole } else { 'N/A' } Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] -> Group found: $($group.DisplayName) (Protected: $protectedStatus)" -ForegroundColor Green $assignmentProps.GroupName = $group.DisplayName $assignmentProps.GroupId = $group.Id $assignmentProps.GroupType = $group.GroupTypes -join ',' $assignmentProps.GroupMembershipType = if ($group.MembershipRule -and $group.MembershipRuleProcessingState -eq 'On') { 'Dynamic' } else { 'Static' } $assignmentProps.IsAssignableToRole = $group.IsAssignableToRole $assignmentProps.IsRoleAssignableGroup = $group.IsAssignableToRole } else { $assignmentProps.AssignmentType = 'Group Assignment (Not Found)' $assignmentProps.GroupName = "Group ID: $($assignment.PrincipalId)" $assignmentProps.GroupId = $assignment.PrincipalId $assignmentProps.GroupType = 'Unknown' $assignmentProps.GroupMembershipType = 'Unknown' $assignmentProps.IsAssignableToRole = $null $assignmentProps.IsRoleAssignableGroup = $null } } catch { Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] -> Error retrieving group $($assignment.PrincipalId): $($_.Exception.Message)" -ForegroundColor Red $assignmentProps.AssignmentType = 'Group Assignment (Error)' $assignmentProps.GroupName = "Group ID: $($assignment.PrincipalId)" $assignmentProps.GroupId = $assignment.PrincipalId $assignmentProps.GroupType = 'Unknown' $assignmentProps.GroupMembershipType = 'Unknown' $assignmentProps.IsAssignableToRole = $null $assignmentProps.IsRoleAssignableGroup = $null } } elseif ($assignment.PrincipalType -eq 'ServicePrincipal') { $assignmentProps.AssignmentType = 'Service Principal Assignment' $assignmentProps.PrincipalType = 'ServicePrincipal' try { $servicePrincipal = Get-MgServicePrincipal -ServicePrincipalId $assignment.PrincipalId -ErrorAction SilentlyContinue if ($servicePrincipal) { Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] -> Service Principal found: $($servicePrincipal.DisplayName)" -ForegroundColor Magenta $assignmentProps.ServicePrincipalName = $servicePrincipal.DisplayName $assignmentProps.ServicePrincipalAppId = $servicePrincipal.AppId $assignmentProps.ServicePrincipalType = $servicePrincipal.ServicePrincipalType $assignmentProps.AssignedServicePrincipalId = $servicePrincipal.Id } else { $assignmentProps.AssignmentType = 'Service Principal Assignment (Not Found)' $assignmentProps.ServicePrincipalName = "Service Principal ID: $($assignment.PrincipalId)" $assignmentProps.AssignedServicePrincipalId = $assignment.PrincipalId $assignmentProps.ServicePrincipalAppId = 'Unknown' $assignmentProps.ServicePrincipalType = 'Unknown' } } catch { Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] -> Error retrieving Service Principal $($assignment.PrincipalId): $($_.Exception.Message)" -ForegroundColor Red $assignmentProps.AssignmentType = 'Service Principal Assignment (Error)' $assignmentProps.ServicePrincipalName = "Service Principal ID: $($assignment.PrincipalId)" $assignmentProps.AssignedServicePrincipalId = $assignment.PrincipalId $assignmentProps.ServicePrincipalAppId = 'Unknown' $assignmentProps.ServicePrincipalType = 'Unknown' } } else { # Handle unknown/unsupported principal types $assignmentProps.AssignmentType = "$($assignment.PrincipalType) Assignment" $assignmentProps.PrincipalType = $assignment.PrincipalType $assignmentProps.PrincipalId = $assignment.PrincipalId $assignmentProps.PrincipalDisplayName = "Unknown $($assignment.PrincipalType)" Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] -> Unhandled principal type: $($assignment.PrincipalType) (ID: $($assignment.PrincipalId))" -ForegroundColor DarkYellow } # Emit the assignment object [PSCustomObject]$assignmentProps } } else { # No assignments found - create one entry using the same complete structure $assignmentProps = [ordered]@{ # Application info (most important first) ApplicationName = $sp.DisplayName ApplicationType = if ($sp.Tags -contains 'WindowsAzureActiveDirectoryIntegratedApp') { 'Enterprise Application' } else { $sp.ServicePrincipalType } AssignmentType = '' PrincipalType = '' IsRoleAssignableGroup = $null # Principal details (User, Group, or Service Principal) UserName = $null UserPrincipalName = $null GroupName = $null GroupType = $null GroupMembershipType = $null ServicePrincipalName = $null ServicePrincipalType = $null PrincipalDisplayName = $null # Role and permission details AppRoleValue = $null AppRoleId = $null AppRoleAssignmentRequired = $sp.AppRoleAssignmentRequired # Technical IDs (less important, at the end) ApplicationId = $sp.AppId ServicePrincipalId = $sp.Id UserId = $null GroupId = $null AssignedServicePrincipalId = $null PrincipalId = $null IsAssignableToRole = $null ServicePrincipalAppId = $null # Metadata (at the end) ApplicationPublisher = $sp.PublisherName CreatedDate = $sp.CreatedDateTime } if (-not $sp.AppRoleAssignmentRequired) { $assignmentProps.AssignmentType = 'All Users (No Assignment Required)' $assignmentProps.PrincipalType = 'All Users' Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] -> Application available to all users (no assignment required)" -ForegroundColor Yellow } else { $assignmentProps.AssignmentType = 'Not Assigned' $assignmentProps.PrincipalType = 'None' Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] -> No assignments found" -ForegroundColor Gray } # Emit the no-assignment object [PSCustomObject]$assignmentProps } } if ($useParallel) { Write-Verbose "Analyzing applications in parallel (ThrottleLimit: $ThrottleLimit)..." $processText = $processSp.ToString() $parallelResults = $servicePrincipals | ForEach-Object -ThrottleLimit $ThrottleLimit -Parallel { Import-Module Microsoft.Graph.Authentication -ErrorAction SilentlyContinue Import-Module Microsoft.Graph.Applications -ErrorAction SilentlyContinue Import-Module Microsoft.Graph.Users -ErrorAction SilentlyContinue Import-Module Microsoft.Graph.Groups -ErrorAction SilentlyContinue $sb = [scriptblock]::Create($using:processText) & $sb $_ } foreach ($result in $parallelResults) { if ($result) { $applicationAssignmentsArray.Add($result) } } } else { $counter = 0 foreach ($sp in $servicePrincipals) { $counter++ $results = & $processSp $sp "[$counter/$($servicePrincipals.Count)] " foreach ($result in $results) { if ($result) { $applicationAssignmentsArray.Add($result) } } } } Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] Analysis completed. Total items found: $($applicationAssignmentsArray.Count)" -ForegroundColor Cyan # Apply filtering if requested if ($AssignmentNotEnforced.IsPresent) { $beforeCount = $applicationAssignmentsArray.Count $applicationAssignmentsArray = $applicationAssignmentsArray | Where-Object { $_.AppRoleAssignmentRequired -eq $false } Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] Filtering applied (AssignmentNotEnforced): $($applicationAssignmentsArray.Count)/$beforeCount items retained" -ForegroundColor Cyan } if ($AssignmentEmpty.IsPresent) { $beforeCount = $applicationAssignmentsArray.Count $applicationAssignmentsArray = $applicationAssignmentsArray | Where-Object { $_.AssignmentType -eq 'All Users (No Assignment Required)' } Write-Host "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] Filtering applied (AssignmentEmpty): $($applicationAssignmentsArray.Count)/$beforeCount items retained" -ForegroundColor Cyan } if ($ExportToExcel.IsPresent) { $now = Get-Date -Format 'yyyy-MM-dd_HHmmss' $excelFilePath = "$($env:userprofile)\$now-MgApplicationAssignment.xlsx" Write-Host -ForegroundColor Cyan "Exporting application assignments to Excel file: $excelFilePath" $applicationAssignmentsArray | Export-Excel -Path $excelFilePath -AutoSize -AutoFilter -WorksheetName 'EntraApplicationAssignments' Write-Host -ForegroundColor Green 'Export completed successfully!' } else { return $applicationAssignmentsArray } } |