From e53ddf03f4cd7fba51f9923d6952b5e1856933e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A5re=20Overg=C3=A5rd?= Date: Fri, 3 Apr 2026 16:43:11 +0200 Subject: [PATCH 1/4] Define functions to export --- EntraReporter/EntraReporter.psd1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/EntraReporter/EntraReporter.psd1 b/EntraReporter/EntraReporter.psd1 index 28a27af..ad2332d 100644 --- a/EntraReporter/EntraReporter.psd1 +++ b/EntraReporter/EntraReporter.psd1 @@ -64,7 +64,8 @@ # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. FunctionsToExport = @( - '*' + 'Get-EntraIdLevel', + 'Get-EntraIdRoleAssignment' ) # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. From 9d16e650414675366eddd11180e3db5ea87a616b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A5re=20Overg=C3=A5rd?= Date: Fri, 3 Apr 2026 16:43:29 +0200 Subject: [PATCH 2/4] Linting and comment based help --- EntraReporter/functions/Get-EntraIdLevel.ps1 | 2 ++ .../functions/Get-EntraIdRoleAssignment.ps1 | 30 +++++++++-------- .../functions/Get-AdministrativeUnit.ps1 | 29 ++++++++++++++-- .../Get-EntraIdGroupScheduleBatch.ps1 | 7 ++-- .../internal/functions/Invoke-GraphBatch.ps1 | 33 ++++++++++++++++++- .../internal/functions/Invoke-GraphPaged.ps1 | 23 ++++++++++++- .../functions/Split-ArrayIntoChunks.ps1 | 27 ++++++++++++++- .../functions/Test-GraphConnection.ps1 | 4 ++- 8 files changed, 131 insertions(+), 24 deletions(-) diff --git a/EntraReporter/functions/Get-EntraIdLevel.ps1 b/EntraReporter/functions/Get-EntraIdLevel.ps1 index df7a618..d09427d 100644 --- a/EntraReporter/functions/Get-EntraIdLevel.ps1 +++ b/EntraReporter/functions/Get-EntraIdLevel.ps1 @@ -24,10 +24,12 @@ .EXAMPLE Get-EntraIdLevel + Returns the license level for the tenant. .EXAMPLE Get-EntraIdLevel -IncludeDetails + Returns the license level along with detailed unit and SKU information. .NOTES diff --git a/EntraReporter/functions/Get-EntraIdRoleAssignment.ps1 b/EntraReporter/functions/Get-EntraIdRoleAssignment.ps1 index b530e7a..c5b21d8 100644 --- a/EntraReporter/functions/Get-EntraIdRoleAssignment.ps1 +++ b/EntraReporter/functions/Get-EntraIdRoleAssignment.ps1 @@ -10,10 +10,12 @@ .EXAMPLE Get-EntraIdRoleAssignment + Retrieves all role assignments and eligibilities in the connected tenant (requires Entra P2). .EXAMPLE Get-EntraIdRoleAssignment -RoleName 'Global Administrator' + Retrieves assignments and eligibilities for the Global Administrator role only. .NOTES @@ -31,7 +33,7 @@ function Get-EntraIdRoleAssignment { $RoleName ) - #region Internal functions + #region Internal functions # Resolve-AssignmentWindow: compute the effective assignment window from principal and user time windows function Resolve-AssignmentWindow { @@ -110,7 +112,7 @@ function Get-EntraIdRoleAssignment { 'Eligible' } - # Emit a single entry for the combination of principal and user with the resolved effective assignment window and state. + # Emit a single entry for the combination of principal and user with the resolved effective assignment window and state. [PSCustomObject]@{ PSTypeName = 'EntraReporter.RoleAssignment' RoleId = $RoleId @@ -144,29 +146,29 @@ function Get-EntraIdRoleAssignment { # Graph schedule object for group role assignment/eligibility [Parameter(Mandatory = $true)] [psobject] - $Schedule, + $Schedule, # Group state context being processed [Parameter(Mandatory = $true)] [ValidateSet('Assigned', 'Eligible')] [string] - $GroupState, + $GroupState, # Resulting user-level state for group members [Parameter(Mandatory = $true)] [ValidateSet('Assigned', 'Eligible')] [string] - $UserState + $UserState ) $principal = $Schedule.principal # Choose group schedule entries based on whether we are resolving assigned or eligible members if ($UserState -eq 'Assigned') { - $scheduleEntries = $script:groupAssignmentSchedules | Where-Object { $_.groupId -eq $principal.id } + $scheduleEntries = $script:groupAssignmentSchedules | Where-Object { $_.groupId -eq $principal.id } } else { - $scheduleEntries = $script:groupEligibilitySchedules | Where-Object { $_.groupId -eq $principal.id } + $scheduleEntries = $script:groupEligibilitySchedules | Where-Object { $_.groupId -eq $principal.id } } foreach ($scheduleEntry in $scheduleEntries ) { @@ -209,13 +211,13 @@ function Get-EntraIdRoleAssignment { # The Graph role schedule to resolve into output entries [Parameter(Mandatory = $true)] [psobject] - $Schedule, + $Schedule, # The schedule state to apply for this resolution pass [Parameter(Mandatory = $true)] [ValidateSet('Assigned', 'Eligible')] [string] - $State + $State ) # Resolve principal to allow handling of different principal types @@ -281,7 +283,7 @@ function Get-EntraIdRoleAssignment { #region MAIN # Always stop on errors to avoid emitting incomplete data. Errors should be handled at the command level to allow for more granular error handling (e.g. skipping individual entries that fail to resolve rather than failing the entire command). - $ErrorActionPreference = 'Stop' + $ErrorActionPreference = 'Stop' if (!(Test-GraphConnection)) { throw 'No active connection to Microsoft Graph. Run Connect-MgGraph to sign in and then retry.' @@ -298,7 +300,7 @@ function Get-EntraIdRoleAssignment { # Fetch all role schedules, assigned and eligible. Write-Progress -Activity $activityName -Status 'Fetching assigned role schedules' -PercentComplete 20 - $roleAssignmentSchedules = Invoke-MgGraphRequest -Method GET -Uri "v1.0/roleManagement/directory/roleAssignmentSchedules?`$filter=assignmentType eq 'Assigned'&`$expand=principal,roleDefinition" -Verbose:$false | Select-Object -ExpandProperty value + $roleAssignmentSchedules = Invoke-MgGraphRequest -Method GET -Uri "v1.0/roleManagement/directory/roleAssignmentSchedules?`$filter=assignmentType eq 'Assigned'&`$expand=principal,roleDefinition" -Verbose:$false | Select-Object -ExpandProperty value Write-Progress -Activity $activityName -Status 'Fetching eligible role schedules' -PercentComplete 40 $roleEligibilitySchedules = Invoke-MgGraphRequest -Method GET -Uri "v1.0/roleManagement/directory/roleEligibilitySchedules?`$expand=principal,roleDefinition" -Verbose:$false | Select-Object -ExpandProperty value @@ -308,7 +310,7 @@ function Get-EntraIdRoleAssignment { $roleEligibilitySchedules = $roleEligibilitySchedules | Where-Object { $_.roleDefinition.displayName -in $RoleName } } - # 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). + # 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 @@ -324,9 +326,9 @@ function Get-EntraIdRoleAssignment { # 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' + $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' + $script:groupEligibilitySchedules = Get-EntraIdGroupScheduleBatch -GroupId $groupIds -State 'Eligible' if (($groupAssignmentSchedules | Where-Object { $_.principal.'@odata.type' -eq '#microsoft.graph.group' }) -or ($groupEligibilitySchedules | Where-Object { $_.principal.'@odata.type' -eq '#microsoft.graph.group' })) { diff --git a/EntraReporter/internal/functions/Get-AdministrativeUnit.ps1 b/EntraReporter/internal/functions/Get-AdministrativeUnit.ps1 index 50968ea..f1f7035 100644 --- a/EntraReporter/internal/functions/Get-AdministrativeUnit.ps1 +++ b/EntraReporter/internal/functions/Get-AdministrativeUnit.ps1 @@ -1,11 +1,34 @@ -# Generate a list of all administrative units in the directory for resolving scope information in role assignments. +<# + .SYNOPSIS + Returns a list of Azure AD administrative units (plus the root directory scope) formatted for EntraReporter. + + .DESCRIPTION + Retrieves all administrative units from Microsoft Graph using Invoke-MgGraphRequest and converts each item into a PSCustomObject with directoryScopeId and displayName. + + .PARAMETER None + This cmdlet does not take any parameters. + + .OUTPUTS + System.Management.Automation.PSCustomObject + Contains properties: + - directoryScopeId: String, e.g. '/administrativeUnits/{id}' or '/' + - displayName: String, administrative unit display name or 'Directory' + + .EXAMPLE + Get-AdministrativeUnit + + Returns all administrative units and the root directory scope. + + .NOTES + Part of EntraReporter internal functions. +#> function Get-AdministrativeUnit { [CmdletBinding()] $results = @() - # Get all administrative units from directory to avoid making multiple calls - $allAdministrativeUnits = Invoke-MgGraphRequest -Method GET -Uri 'v1.0/directory/administrativeUnits' -Verbose:$false | Select-Object -ExpandProperty value + # Get all administrative units from directory to avoid making multiple calls + $allAdministrativeUnits = Invoke-MgGraphRequest -Method GET -Uri 'v1.0/directory/administrativeUnits' -Verbose:$false | Select-Object -ExpandProperty value foreach ($adminUnit in $allAdministrativeUnits) { $results += [pscustomobject] @{ directoryScopeId = "/administrativeUnits/$($adminUnit.id)" diff --git a/EntraReporter/internal/functions/Get-EntraIdGroupScheduleBatch.ps1 b/EntraReporter/internal/functions/Get-EntraIdGroupScheduleBatch.ps1 index 35466da..5293eac 100644 --- a/EntraReporter/internal/functions/Get-EntraIdGroupScheduleBatch.ps1 +++ b/EntraReporter/internal/functions/Get-EntraIdGroupScheduleBatch.ps1 @@ -19,6 +19,7 @@ .EXAMPLE Get-EntraIdGroupScheduleBatch -GroupId @('group-id-1', 'group-id-2') -State 'Assigned' + Retrieves active membership assignment schedules for the specified groups. .NOTES @@ -31,13 +32,13 @@ function Get-EntraIdGroupScheduleBatch { # Array of Azure AD group IDs to fetch schedules for [Parameter(Mandatory = $true)] [string[]] - $GroupId, + $GroupId, # Type of schedule: 'Assigned' for active, 'Eligible' for eligible memberships [Parameter(Mandatory = $true)] [ValidateSet('Assigned', 'Eligible')] [string] - $State + $State ) # Select the appropriate Graph API endpoint based on the requested state @@ -51,7 +52,7 @@ function Get-EntraIdGroupScheduleBatch { $urlTemplate = "/identityGovernance/privilegedAccess/group/eligibilitySchedules?`$filter=groupId eq '{Id}'&`$expand=principal" } } - + # Split the GroupId array into chunks of 20 for batch processing (Graph API batch limit optimization) $chunks = Split-ArrayIntoChunks -InputObject $GroupId -ChunkSize 20 diff --git a/EntraReporter/internal/functions/Invoke-GraphBatch.ps1 b/EntraReporter/internal/functions/Invoke-GraphBatch.ps1 index 1fd9238..84eb906 100644 --- a/EntraReporter/internal/functions/Invoke-GraphBatch.ps1 +++ b/EntraReporter/internal/functions/Invoke-GraphBatch.ps1 @@ -1,4 +1,35 @@ -function Invoke-GraphBatch { +<# + .SYNOPSIS + Executes a Microsoft Graph batch request and optionally fails on any non-success status code. + + .DESCRIPTION + Sends a batch of individual Graph API requests in a single HTTP POST to reduce round-trips. Returns the full Graph batch response. + + .PARAMETER Requests + An array of hashtables representing Graph batch requests. Each hashtable must include id, method, and url. + + .PARAMETER GraphVersion + Graph API version to call (default: 'v1.0'). + + .PARAMETER ThrowOnAnyError + If specified, throws an exception when any response entry has HTTP status 400 or greater. + + .OUTPUTS + System.Object (Graph batch response) + + .EXAMPLE + $batch = @( + @{ id = '1'; method = 'GET'; url = '/users' } + @{ id = '2'; method = 'GET'; url = '/groups' } + ) + Invoke-GraphBatch -Requests $batch -GraphVersion 'v1.0' -ThrowOnAnyError + + Performs the specified batch request against Microsoft Graph and throws if any individual request fails. + + .NOTES + Used by EntraReporter internal routines to perform Graph batch calls. +#> +function Invoke-GraphBatch { [CmdletBinding()] param( [Parameter(Mandatory)] diff --git a/EntraReporter/internal/functions/Invoke-GraphPaged.ps1 b/EntraReporter/internal/functions/Invoke-GraphPaged.ps1 index 635e834..c2a510e 100644 --- a/EntraReporter/internal/functions/Invoke-GraphPaged.ps1 +++ b/EntraReporter/internal/functions/Invoke-GraphPaged.ps1 @@ -1,4 +1,25 @@ -# Robust pagination helper +<# + .SYNOPSIS + Retrieves all items from a paged Microsoft Graph endpoint by following @odata.nextLink. + + .DESCRIPTION + Performs repeated GET calls to the provided Graph URI and collects results from each page. Appends all `value` arrays into a single output array. This supports Graph endpoints that return paged data. + + .PARAMETER Uri + The initial Microsoft Graph URI to request (e.g. 'https://graph.microsoft.com/v1.0/users'). + + .OUTPUTS + System.Object[] + An array of objects returned from Graph pages. + + .EXAMPLE + Invoke-GraphPaged -Uri 'https://graph.microsoft.com/v1.0/users' + + Returns all users from the tenant by following pagination links. + + .NOTES + Uses Invoke-MgGraphRequest to perform each request and expects standard Graph pagination (`@odata.nextLink`). +#> function Invoke-GraphPaged { param( [Parameter(Mandatory)][string] $Uri diff --git a/EntraReporter/internal/functions/Split-ArrayIntoChunks.ps1 b/EntraReporter/internal/functions/Split-ArrayIntoChunks.ps1 index 677fc84..fe6234c 100644 --- a/EntraReporter/internal/functions/Split-ArrayIntoChunks.ps1 +++ b/EntraReporter/internal/functions/Split-ArrayIntoChunks.ps1 @@ -1,4 +1,29 @@ -function Split-ArrayIntoChunks { +<# + .SYNOPSIS + Splits an array into multiple chunks of a specified size. + + .DESCRIPTION + Takes an input array and divides it into smaller arrays (chunks). Each returned chunk is a fixed-size object[] (except the last chunk) and the function returns a collection of chunk arrays. Useful for batching operations. + + .PARAMETER InputObject + The array of values to split into chunks. Cannot be null or empty. + + .PARAMETER ChunkSize + The maximum number of items per chunk. Must be an integer greater than or equal to 1. + + .OUTPUTS + System.Collections.Generic.List[object[]] + A list of object arrays, where each array is a chunk from the original input. + + .EXAMPLE + Split-ArrayIntoChunks -InputObject @(1,2,3,4,5) -ChunkSize 2 + + Returns chunks: @(1,2), @(3,4), @(5) + + .NOTES + Used by EntraReporter helpers for batching Graph requests. +#> +function Split-ArrayIntoChunks { [CmdletBinding()] param( [Parameter(Mandatory)] diff --git a/EntraReporter/internal/functions/Test-GraphConnection.ps1 b/EntraReporter/internal/functions/Test-GraphConnection.ps1 index 6bb9c3b..aec16ec 100644 --- a/EntraReporter/internal/functions/Test-GraphConnection.ps1 +++ b/EntraReporter/internal/functions/Test-GraphConnection.ps1 @@ -2,9 +2,11 @@ .SYNOPSIS Test Microsoft Graph connection .DESCRIPTION - Verifies that the Microsoft.Graph.Authentication module is installed and that the session is connected to Microsoft Graph. + Verifies that the Microsoft.Graph.Authentication module is installed and that the session is connected to Microsoft Graph. .EXAMPLE Test-GraphConnection + + Returns true if connected to Graph, false otherwise. Used by EntraReporter internal routines to check Graph connectivity before making API calls. #> function Test-GraphConnection { [CmdletBinding()] From 974006933cc7aef7ac134c9400a1d90ad19a2338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A5re=20Overg=C3=A5rd?= Date: Fri, 3 Apr 2026 16:46:37 +0200 Subject: [PATCH 3/4] Fix release date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bce4ae..eb4d5ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ 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.1.0 (2026-03-28) +## 0.1.0 (2026-04-03) ### Added From 2e66e019b4a552572d19481622dbd5479ac835de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A5re=20Overg=C3=A5rd?= Date: Fri, 3 Apr 2026 16:46:52 +0200 Subject: [PATCH 4/4] Remove internal functions --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 27148c7..5a10587 100644 --- a/README.md +++ b/README.md @@ -84,12 +84,6 @@ P1SkuPartNumbers: {AAD_PREMIUM} P2SkuPartNumbers: {AAD_PREMIUM_P2} ``` -### Other Functions - -- **Get-EntraIdGroupScheduleBatch:** Retrieves group membership schedules in batches for efficiency. -- **Get-AdministrativeUnit:** Internal function for resolving administrative unit names. -- **Invoke-GraphBatch / Invoke-GraphPaged:** Internal utilities for Graph API calls. - ## Contributing Contributions are welcome! Please submit issues and pull requests on GitHub.