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
12 changes: 11 additions & 1 deletion Module/IntuneAssignmentChecker/IntuneAssignmentChecker.psd1
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@{
RootModule = 'IntuneAssignmentChecker.psm1'
ModuleVersion = '4.3.0'
ModuleVersion = '4.3.1'
GUID = 'c6e25ec6-5787-45ef-95af-8abeb8a17daf'
Author = 'Ugur Koc'
CompanyName = 'Community'
Expand Down Expand Up @@ -43,6 +43,16 @@
ProjectUri = 'https://github.com/ugurkocde/IntuneAssignmentChecker'
IconUri = ''
ReleaseNotes = @'
Version 4.3.1:
Security:
- Replace the broad Group.Read.All permission with GroupMember.Read.All for group lookup, membership, and transitive membership operations. This preserves IntuneAssignmentChecker behavior without granting access to Microsoft 365 group content.

Upgrade notes:
- Interactive connections automatically request GroupMember.Read.All on the next sign-in. Administrator consent is still required.
- Existing app-only registrations that use a certificate, client secret, managed identity, or pre-fetched token must add GroupMember.Read.All and grant administrator consent before Group.Read.All is removed and revoked.
- Updating requiredResourceAccess alone might not remove an existing service principal consent grant. Confirm that Group.Read.All is revoked after the updated module works.
- Member.Read.Hidden remains optional and is needed only when hidden-membership groups must be resolved.

Version 4.3.0:
- Show applications where the checked group is excluded in Get-IntuneGroupAssignment; Compare-IntuneGroupAssignment now marks excluded apps with [EXCLUDED] (issue #126).
- Rebuild the ten category-walk cmdlets on a shared scan engine: entity sets are fetched once per run (dozens fewer Graph calls), transient per-category failures no longer abort a run, and errors are raised on the error stream for automation (issue #123).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ $script:IntentTemplateSubtypeToFamily = @{
# Required Microsoft Graph permissions (shared by Connect-IntuneAssignmentChecker and Switch-Tenant)
$script:RequiredPermissions = @(
@{ Permission = "User.Read.All"; Reason = "Required to read user profile information and check group memberships" }
@{ Permission = "Group.Read.All"; Reason = "Needed to read group information and memberships" }
@{ Permission = "GroupMember.Read.All"; Reason = "Required to read group memberships and basic group properties" }
@{ Permission = "DeviceManagementConfiguration.Read.All"; Reason = "Allows reading Intune device configuration policies and their assignments" }
@{ Permission = "DeviceManagementApps.Read.All"; Reason = "Necessary to read mobile app management policies and app configurations" }
@{ Permission = "DeviceManagementManagedDevices.Read.All"; Reason = "Required to read managed device information and compliance policies" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function Connect-IntuneAssignmentChecker {
Write-Host "INTUNE ASSIGNMENT CHECKER" -ForegroundColor Cyan
Write-Host "Made by Ugur Koc" -NoNewline
Write-Host " | Version" -NoNewline; Write-Host " $localVersion" -ForegroundColor Yellow -NoNewline
Write-Host " | Last updated: " -NoNewline; Write-Host "2026-04-28" -ForegroundColor Magenta
Write-Host " | Last updated: " -NoNewline; Write-Host "2026-07-23" -ForegroundColor Magenta
Write-Host ""
Write-Host "Feedback & Issues: " -NoNewline -ForegroundColor Cyan
Write-Host "https://github.com/ugurkocde/IntuneAssignmentChecker/issues" -ForegroundColor White
Expand Down
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,11 @@ IntuneAssignmentChecker
### Required Permissions

Your Entra ID application registration needs these permissions:

| Permission | Type | Description |
|------------|------|-------------|
| User.Read.All | Application | Read all users' full profiles |
| Group.Read.All | Application | Read all groups |
| GroupMember.Read.All | Application | Read group memberships and basic group properties |
| Device.Read.All | Application | Read all devices |
| DeviceManagementApps.Read.All | Application | Read Microsoft Intune apps |
| DeviceManagementConfiguration.Read.All | Application | Read Microsoft Intune device configuration and policies |
Expand All @@ -124,6 +125,16 @@ Your Entra ID application registration needs these permissions:
| CloudPC.Read.All | Application | Read Windows 365 Cloud PC provisioning policies and settings |
| DeviceManagementRBAC.Read.All | Application | Read role scope tags for scope tag display and filtering |

For interactive authentication, IntuneAssignmentChecker automatically requests the delegated versions of these permissions during sign-in. Administrator consent is still required.

For certificate, client secret, managed identity, or pre-fetched token authentication, configure the listed application permissions on the app registration and grant administrator consent. App-only authentication cannot add or consent permissions automatically.

`GroupMember.Read.All` provides the group and membership data used by IntuneAssignmentChecker without granting access to Microsoft 365 group content.

> **Existing app registrations**: Add `GroupMember.Read.All` and grant administrator consent before removing `Group.Read.All`. After confirming the updated module works, remove `Group.Read.All` from the configured API permissions and revoke its application consent. Updating the app registration manifest alone might not remove an existing service principal consent grant.

> **Hidden memberships**: Reading groups with hidden membership requires the additional `Member.Read.Hidden` application permission. IntuneAssignmentChecker does not request this permission by default.

> **Note**: The automated setup script ([`Register-IntuneAssignmentCheckerApp.ps1`](./Register-IntuneAssignmentCheckerApp.ps1)) additionally grants `DeviceManagementServiceConfig.Read.All`, which covers Intune service configuration such as enrollment settings. It is not validated by `Connect-IntuneAssignmentChecker`, but granting it avoids gaps when reading enrollment-related configurations.

## 🔐 Authentication Options
Expand Down
3 changes: 1 addition & 2 deletions Register-IntuneAssignmentCheckerApp.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Write-Host "Short Tenant Name: $shortTenantName" -ForegroundColor Green
$graphAppId = "00000003-0000-0000-c000-000000000000"
$permissions = @(
@{ id = "df021288-bdef-4463-88db-98f22de89214"; displayName = "User.Read.All" },
@{ id = "5b567255-7703-4780-807c-7be8301ae99b"; displayName = "Group.Read.All" },
@{ id = "98830695-27a2-44f7-8c18-0c3ebc9698f6"; displayName = "GroupMember.Read.All" },
@{ id = "7438b122-aefc-4978-80ed-43db9fcc7715"; displayName = "Device.Read.All" },
@{ id = "7a6ee1e7-141e-4cec-ae74-d9db155731ff"; displayName = "DeviceManagementApps.Read.All" },
@{ id = "dc377aa6-52d8-4e23-b271-2a7ae04cedf3"; displayName = "DeviceManagementConfiguration.Read.All" },
Expand Down Expand Up @@ -143,4 +143,3 @@ Write-Host "You can now connect IntuneAssignmentChecker with the following comma
Write-Host "Connect-IntuneAssignmentChecker -AppId `"$appId`" -TenantId `"$tenantId`" -CertificateThumbprint `"$certificateThumbprint`"" -ForegroundColor Yellow
Write-Host "Afterwards, run 'IntuneAssignmentChecker' to start the interactive menu." -ForegroundColor Cyan
Write-Host "----------------------------"

4 changes: 4 additions & 0 deletions Tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ network. Runs in well under a second.
filter-suffixed status strings
- `Add-ExportData` - regex extraction of FilterName and FilterType from
AssignmentReason into dedicated CSV columns
- Permission configuration - verifies the registration script, runtime check,
and README use the least-privilege `GroupMember.Read.All` application role
- Group membership Graph helpers - verifies the transitive group membership
endpoint and pagination behavior

**Why these tests matter:** most regressions in this codebase are string-format
changes that slip past static analysis. Unit tests at this layer catch them.
Expand Down
55 changes: 55 additions & 0 deletions Tests/Unit/GraphMembership.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#Requires -Version 7.0
#Requires -Modules @{ ModuleName='Pester'; ModuleVersion='5.0.0' }

BeforeAll {
$modulePrivate = Join-Path $PSScriptRoot '../../Module/IntuneAssignmentChecker/Private'

function Invoke-MgGraphRequest {
param([string]$Uri, [string]$Method)
$null = $Uri
$null = $Method
@{ value = @() }
}

. (Join-Path $modulePrivate 'Get-TransitiveGroupMembership.ps1')

$script:GraphEndpoint = 'https://graph.test'
}

Describe 'Get-TransitiveGroupMembership' {
BeforeEach {
$script:requestedUris = [System.Collections.Generic.List[string]]::new()

Mock Invoke-MgGraphRequest {
$script:requestedUris.Add($Uri)

if ($Uri -eq 'https://graph.test/v1.0/groups/group-1/transitiveMemberOf/microsoft.graph.group?$select=id,displayName') {
return @{
value = @(
[PSCustomObject]@{ id = 'parent-1'; displayName = 'Parent One' }
)
'@odata.nextLink' = 'https://graph.test/v1.0/groups/group-1/transitiveMemberOf/microsoft.graph.group?$skiptoken=next'
}
}

return @{
value = @(
[PSCustomObject]@{ id = 'parent-2'; displayName = 'Parent Two' }
)
}
}
}

It 'uses the tested transitive group endpoint and follows pagination' {
$result = @(Get-TransitiveGroupMembership -GroupId 'group-1')

$result.Count | Should -Be 2
$result[0].id | Should -BeExactly 'parent-1'
$result[1].id | Should -BeExactly 'parent-2'
$script:requestedUris | Should -Be @(
'https://graph.test/v1.0/groups/group-1/transitiveMemberOf/microsoft.graph.group?$select=id,displayName'
'https://graph.test/v1.0/groups/group-1/transitiveMemberOf/microsoft.graph.group?$skiptoken=next'
)
Should -Invoke Invoke-MgGraphRequest -Exactly 2 -ParameterFilter { $Method -eq 'Get' }
}
}
32 changes: 32 additions & 0 deletions Tests/Unit/PermissionConfiguration.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#Requires -Version 7.0
#Requires -Modules @{ ModuleName='Pester'; ModuleVersion='5.0.0' }

BeforeAll {
$repoRoot = Join-Path $PSScriptRoot '../..'
$script:registrationScript = Get-Content -Path (Join-Path $repoRoot 'Register-IntuneAssignmentCheckerApp.ps1') -Raw
$script:moduleScript = Get-Content -Path (Join-Path $repoRoot 'Module/IntuneAssignmentChecker/IntuneAssignmentChecker.psm1') -Raw
$script:readme = Get-Content -Path (Join-Path $repoRoot 'README.md') -Raw
}

Describe 'Least-privilege group permission configuration' {
It 'configures GroupMember.Read.All as an application role' {
$script:registrationScript | Should -Match '98830695-27a2-44f7-8c18-0c3ebc9698f6'
$script:registrationScript | Should -Match 'displayName\s*=\s*"GroupMember\.Read\.All"'
$script:registrationScript | Should -Match '\$permissions \| ForEach-Object \{ @\{ id = \$_.id; type = "Role" \} \}'
}

It 'does not configure the broader Group.Read.All application role' {
$script:registrationScript | Should -Not -Match '5b567255-7703-4780-807c-7be8301ae99b'
$script:registrationScript | Should -Not -Match 'displayName\s*=\s*"Group\.Read\.All"'
}

It 'requires GroupMember.Read.All at runtime' {
$script:moduleScript | Should -Match 'Permission\s*=\s*"GroupMember\.Read\.All"'
$script:moduleScript | Should -Not -Match 'Permission\s*=\s*"Group\.Read\.All"'
}

It 'documents the least-privilege permission and migration path' {
$script:readme | Should -Match '\|\s*GroupMember\.Read\.All\s*\|\s*Application\s*\|'
$script:readme | Should -Match 'Add `GroupMember\.Read\.All` and grant administrator consent before removing `Group\.Read\.All`'
}
}
Loading