From 3558ebe36bac96b77b3959b5414e12f6cc3d8f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A5re=20Overg=C3=A5rd?= Date: Sat, 4 Apr 2026 19:47:52 +0200 Subject: [PATCH] Fix scopes in documentation. Better error handling --- CHANGELOG.md | 11 ++ EntraReporter/EntraReporter.psd1 | 8 +- .../functions/Get-EntraIdRoleAssignment.ps1 | 123 +++++++++--------- README.md | 2 +- 4 files changed, 82 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb4d5ff..ee935f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.2.0 (2026-04-04) + +### Fixed + +- List of required Graph scopes was missing scopes. + +### Added + +- Better error handling due to missing permissions or unknown objects +- Requirement for module 'Microsoft.Graph.Authentication' + ## 0.1.0 (2026-04-03) ### Added diff --git a/EntraReporter/EntraReporter.psd1 b/EntraReporter/EntraReporter.psd1 index 3402230..ca164a0 100644 --- a/EntraReporter/EntraReporter.psd1 +++ b/EntraReporter/EntraReporter.psd1 @@ -4,7 +4,7 @@ RootModule = 'EntraReporter.psm1' # Version number of this module. - ModuleVersion = '0.1.0' + ModuleVersion = '0.2.0' # Supported PSEditions # CompatiblePSEditions = @() @@ -43,7 +43,9 @@ # ProcessorArchitecture = '' # Modules that must be imported into the global environment prior to importing this module - # RequiredModules = @() + RequiredModules = @( + 'Microsoft.Graph.Authentication' + ) # Assemblies that must be loaded prior to importing this module # RequiredAssemblies = @() @@ -92,7 +94,7 @@ PSData = @{ # Tags applied to this module. These help with module discovery in online galleries. - # Tags = @() + Tags = 'Identity', 'Entra', 'EntraId', 'AzureAD', 'AAD', 'Reporting', 'Graph', 'MicrosoftGraph' # A URL to the license for this module. LicenseUri = 'https://github.com/kovergard/EntraReporter/blob/main/LICENSE' diff --git a/EntraReporter/functions/Get-EntraIdRoleAssignment.ps1 b/EntraReporter/functions/Get-EntraIdRoleAssignment.ps1 index 264ccdb..97dd39e 100644 --- a/EntraReporter/functions/Get-EntraIdRoleAssignment.ps1 +++ b/EntraReporter/functions/Get-EntraIdRoleAssignment.ps1 @@ -224,59 +224,65 @@ function Get-EntraIdRoleAssignment { # Resolve principal to allow handling of different principal types # (user, servicePrincipal, group) and convert each to normalized rows. - $principal = $Schedule.principal + try { + $principal = $Schedule.principal - if ($principal.'@odata.type' -eq '#microsoft.graph.user') { - # User principal; create entry with user as both principal and assignee - $roleEntrySplat = @{ - RoleId = $Schedule.roleDefinitionId - RoleName = $Schedule.roleDefinition.displayName - PrincipalId = $Schedule.principal.id - PrincipalDisplayName = $Schedule.principal.displayName - AssignmentType = 'User' - Scope = $administrativeUnits | Where-Object { $_.directoryScopeId -eq $Schedule.directoryScopeId } | Select-Object -ExpandProperty displayName - PrincipalState = $State - PrincipalStartTime = if ($Schedule.scheduleInfo.expiration.endDateTime) { $Schedule.scheduleInfo.startDateTime } else { $null } - PrincipalEndTime = if ($Schedule.scheduleInfo.expiration.endDateTime) { $Schedule.scheduleInfo.expiration.endDateTime } else { $null } - UserId = $Schedule.principalId - UserDisplayName = $Schedule.principal.displayName - UserPrincipalName = $Schedule.principal.userPrincipalName - UserState = $State - UserStartTime = if ($Schedule.scheduleInfo.expiration.endDateTime) { $Schedule.scheduleInfo.startDateTime } else { $null } - UserEndTime = if ($Schedule.scheduleInfo.expiration.endDateTime) { $Schedule.scheduleInfo.expiration.endDateTime } else { $null } + if ($principal.'@odata.type' -eq '#microsoft.graph.user') { + # User principal; create entry with user as both principal and assignee + $roleEntrySplat = @{ + RoleId = $Schedule.roleDefinitionId + RoleName = $Schedule.roleDefinition.displayName + PrincipalId = $Schedule.principal.id + PrincipalDisplayName = $Schedule.principal.displayName + AssignmentType = 'User' + Scope = $administrativeUnits | Where-Object { $_.directoryScopeId -eq $Schedule.directoryScopeId } | Select-Object -ExpandProperty displayName + PrincipalState = $State + PrincipalStartTime = if ($Schedule.scheduleInfo.expiration.endDateTime) { $Schedule.scheduleInfo.startDateTime } else { $null } + PrincipalEndTime = if ($Schedule.scheduleInfo.expiration.endDateTime) { $Schedule.scheduleInfo.expiration.endDateTime } else { $null } + UserId = $Schedule.principalId + UserDisplayName = $Schedule.principal.displayName + UserPrincipalName = $Schedule.principal.userPrincipalName + UserState = $State + UserStartTime = if ($Schedule.scheduleInfo.expiration.endDateTime) { $Schedule.scheduleInfo.startDateTime } else { $null } + UserEndTime = if ($Schedule.scheduleInfo.expiration.endDateTime) { $Schedule.scheduleInfo.expiration.endDateTime } else { $null } + } + New-RoleAssignmentEntry @roleEntrySplat } - New-RoleAssignmentEntry @roleEntrySplat - } - elseif ($principal.'@odata.type' -eq '#microsoft.graph.servicePrincipal') { - # Service principal; create entry with appId as principal name - $roleEntrySplat = @{ - RoleId = $Schedule.roleDefinitionId - RoleName = $Schedule.roleDefinition.displayName - PrincipalId = $Schedule.principal.id - PrincipalDisplayName = $Schedule.principal.displayName - AssignmentType = 'Service principal' - Scope = $administrativeUnits | Where-Object { $_.directoryScopeId -eq $Schedule.directoryScopeId } | Select-Object -ExpandProperty displayName - PrincipalState = $State - PrincipalStartTime = if ($Schedule.scheduleInfo.expiration.endDateTime) { $Schedule.scheduleInfo.startDateTime } else { $null } - PrincipalEndTime = if ($Schedule.scheduleInfo.expiration.endDateTime) { $Schedule.scheduleInfo.expiration.endDateTime } else { $null } - UserId = $Schedule.principalId - UserDisplayName = $Schedule.principal.displayName - UserPrincipalName = $Schedule.principal.appId - UserState = $State - UserStartTime = if ($Schedule.scheduleInfo.expiration.endDateTime) { $Schedule.scheduleInfo.startDateTime } else { $null } - UserEndTime = if ($Schedule.scheduleInfo.expiration.endDateTime) { $Schedule.scheduleInfo.expiration.endDateTime } else { $null } + elseif ($principal.'@odata.type' -eq '#microsoft.graph.servicePrincipal') { + # Service principal; create entry with appId as principal name + $roleEntrySplat = @{ + RoleId = $Schedule.roleDefinitionId + RoleName = $Schedule.roleDefinition.displayName + PrincipalId = $Schedule.principal.id + PrincipalDisplayName = $Schedule.principal.displayName + AssignmentType = 'Service principal' + Scope = $administrativeUnits | Where-Object { $_.directoryScopeId -eq $Schedule.directoryScopeId } | Select-Object -ExpandProperty displayName + PrincipalState = $State + PrincipalStartTime = if ($Schedule.scheduleInfo.expiration.endDateTime) { $Schedule.scheduleInfo.startDateTime } else { $null } + PrincipalEndTime = if ($Schedule.scheduleInfo.expiration.endDateTime) { $Schedule.scheduleInfo.expiration.endDateTime } else { $null } + UserId = $Schedule.principalId + UserDisplayName = $Schedule.principal.displayName + UserPrincipalName = $Schedule.principal.appId + UserState = $State + UserStartTime = if ($Schedule.scheduleInfo.expiration.endDateTime) { $Schedule.scheduleInfo.startDateTime } else { $null } + UserEndTime = if ($Schedule.scheduleInfo.expiration.endDateTime) { $Schedule.scheduleInfo.expiration.endDateTime } else { $null } + } + New-RoleAssignmentEntry @roleEntrySplat } - New-RoleAssignmentEntry @roleEntrySplat - } - elseif ($principal.'@odata.type' -eq '#microsoft.graph.group') { - # Group principal; expand into per-member entries, handling both assigned and eligible - Resolve-RoleAssignedGroup -Schedule $Schedule -GroupState $State -UserState 'Assigned' - Resolve-RoleAssignedGroup -Schedule $Schedule -GroupState $State -UserState 'Eligible' + elseif ($principal.'@odata.type' -eq '#microsoft.graph.group') { + # Group principal; expand into per-member entries, handling both assigned and eligible + Resolve-RoleAssignedGroup -Schedule $Schedule -GroupState $State -UserState 'Assigned' + Resolve-RoleAssignedGroup -Schedule $Schedule -GroupState $State -UserState 'Eligible' + } + else { + Write-Warning "Principal with ID '$($principal.id)' is of type $($principal.'@odata.type'), which is currently not supported by the command. Skipping entry." + $Schedule | ConvertTo-Json -Depth 5 -Compress | Write-Verbose + } } - else { - Write-Warning "Principal with ID '$($principal.id)' is of type $($principal.'@odata.type'), which is currently not supported by the command. Skipping entry." - $Schedule | ConvertTo-Json -Depth 5 | Write-Verbose + catch { + Write-Warning "An error occurred while processing principal with ID '$($principal.id)' for role assignment with role ID '$($Schedule.roleDefinitionId)'. Skipping entry. Error details: $_" + $Schedule | ConvertTo-Json -Depth 5 -Compress | Write-Verbose } } @@ -314,10 +320,6 @@ function Get-EntraIdRoleAssignment { # If any scopes are used in the role schedules (i.e. scope is not just the entire directory), fetch information about the scopes to allow for better reporting (e.g. resolving administrative unit names). Write-Progress -Activity $activityName -Status 'Fetching scope information' -PercentComplete 60 - $scopeIds = @() - $scopeIds += $roleAssignmentSchedules | Select-Object -ExpandProperty directoryScopeId - $scopeIds += $roleEligibilitySchedules | Select-Object -ExpandProperty directoryScopeId - $scopeIds = $scopeIds | Where-Object { $_.id -ne '/' } | Select-Object -Unique $script:administrativeUnits = Get-AdministrativeUnit # Extract unique group IDs from all role schedules for prefetching group schedule information in bulk to reduce number of API calls later on. @@ -327,11 +329,16 @@ function Get-EntraIdRoleAssignment { $groupIds = $groupIds | Select-Object -Unique # Prefetch group schedules for all groups used groups - Write-Progress -Activity $activityName -Status 'Fetching assigned group schedules' -PercentComplete 65 - $script:groupAssignmentSchedules = Get-EntraIdGroupScheduleBatch -GroupId $groupIds -State 'Assigned' - Write-Progress -Activity $activityName -Status 'Fetching eligible group schedules' -PercentComplete 80 - $script:groupEligibilitySchedules = Get-EntraIdGroupScheduleBatch -GroupId $groupIds -State 'Eligible' - + if ($groupIds.Count -gt 0) { + Write-Progress -Activity $activityName -Status 'Fetching assigned group schedules' -PercentComplete 65 + $script:groupAssignmentSchedules = Get-EntraIdGroupScheduleBatch -GroupId $groupIds -State 'Assigned' + Write-Progress -Activity $activityName -Status 'Fetching eligible group schedules' -PercentComplete 80 + $script:groupEligibilitySchedules = Get-EntraIdGroupScheduleBatch -GroupId $groupIds -State 'Eligible' + } + else { + $script:groupAssignmentSchedules = @() + $script:groupEligibilitySchedules = @() + } if (($groupAssignmentSchedules | Where-Object { $_.principal.'@odata.type' -eq '#microsoft.graph.group' }) -or ($groupEligibilitySchedules | Where-Object { $_.principal.'@odata.type' -eq '#microsoft.graph.group' })) { Write-Warning 'One or more role assignment uses nested groups, which is currently not supported by the command. This may lead to incomplete reporting.' @@ -353,4 +360,4 @@ function Get-EntraIdRoleAssignment { Write-Verbose "Total execution time: $($timer.Elapsed.TotalSeconds) seconds" } -#endregion \ No newline at end of file +#endregion MAIN \ No newline at end of file diff --git a/README.md b/README.md index 5a10587..99de13a 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Returns a collection of PSCustomObjects with properties including RoleId, RoleNa ![image](Images/Get-EntraIdRoleAssignment-Format-List.png) **Notes:** -Requires Entra P2 license level. Ensure you have connected to Microsoft Graph with appropriate scopes (e.g., `Connect-MgGraph -Scopes 'RoleEligibilitySchedule.Read.Directory','PrivilegedEligibilitySchedule.Read.AzureADGroup', 'PrivilegedAssignmentSchedule.Read.AzureADGroup'`). +Requires Entra P2 license level. Ensure you have connected to Microsoft Graph with appropriate scopes (e.g., `Connect-MgGraph -Scopes 'RoleEligibilitySchedule.Read.Directory','RoleAssignmentSchedule.Read.Directory', 'PrivilegedEligibilitySchedule.Read.AzureADGroup', 'PrivilegedAssignmentSchedule.Read.AzureADGroup', 'LicenseAssignment.Read.All', 'AdministrativeUnit.Read.All', 'Application.Read.All'`). ### Get-EntraIdLevel