Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion EntraReporter/EntraReporter.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions EntraReporter/functions/Get-EntraIdLevel.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 16 additions & 14 deletions EntraReporter/functions/Get-EntraIdRoleAssignment.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 ) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.'
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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' })) {
Expand Down
29 changes: 26 additions & 3 deletions EntraReporter/internal/functions/Get-AdministrativeUnit.ps1
Original file line number Diff line number Diff line change
@@ -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)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand Down
33 changes: 32 additions & 1 deletion EntraReporter/internal/functions/Invoke-GraphBatch.ps1
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down
23 changes: 22 additions & 1 deletion EntraReporter/internal/functions/Invoke-GraphPaged.ps1
Original file line number Diff line number Diff line change
@@ -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
Expand Down
27 changes: 26 additions & 1 deletion EntraReporter/internal/functions/Split-ArrayIntoChunks.ps1
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down
4 changes: 3 additions & 1 deletion EntraReporter/internal/functions/Test-GraphConnection.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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()]
Expand Down
6 changes: 0 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading