Skip to content

Commit b787805

Browse files
authored
v4.3: Excluded apps in group checks, shared category-scan engine, docs alignment (#127)
* Fix private helper bugs: undefined vars, missing pagination, session exit, caching, UPN encoding (#123) - Promote required permissions to module scope; fix Switch-Tenant connecting with empty scopes (F11) - Refresh assignment filter lookup and clear group cache on tenant switch (F24) - Paginate Get-GroupMemberships transitiveMemberOf results (F12) - Set-Environment returns null instead of killing the PowerShell session (F15) - Cache Get-GroupInfo lookups per tenant (F26) - URL-encode UPNs so guest accounts with # resolve (F27) - Paginate Get-AssignmentFailures policy and status fetches (F20) - Export-ResultsIfRequested takes ExportToCSV/ParameterMode as explicit parameters (F17) * Fix public cmdlet bugs: OData escaping, app protection membership, intent misclassification, broken exports, empty-group checks, pagination (#123) - Escape single quotes in group-name OData filters (F9) - App Protection policies now filtered by actual group membership instead of undefined GroupId (F13) - Use tracked winning assignment for app intent classification (F14) - Fix nested Add-ExportData parenthesization producing junk CSV rows in three cmdlets; fix wrong Device entry and invalid -GetExtra argument in group export (F22) - Get-IntuneEmptyGroup: App Protection section now actually tests for empty groups; populate the six endpoint security categories that were displayed but never fetched; add missing Account Protection display section (F18, F19) - Compare-IntuneGroupAssignment: paginate Settings Catalog, Compliance, and Apps fetches (F25) - Update-IntuneSettingDefinition writes to LocalApplicationData instead of the module install directory; Search-IntuneSetting prefers the user copy (F28) * Add CmdletBinding to all 34 private functions (#123) Enables common parameters (-Verbose, -ErrorAction) and PSCmdlet error handling in private helpers. Verified no caller-scope variable reads break under advanced-function scoping. * Harden auth input, HTML encoding, and register script; fix EDR policies missing from HTML report (#123) - Add -ClientSecretCredential (PSCredential) to Connect/Invoke, preferred over plain-string -ClientSecret which is retained for compatibility (F6) - Derive version banner from module metadata instead of a duplicated literal (F29) - HTML-encode all Graph-derived values in HTML report table cells (F21) - Remove html-export.ps1 redefinitions of module functions and variables; rename its divergent Get-AssignmentInfo to Get-HtmlAssignmentInfo; the removed local template mapping carried a typo that silently dropped EDR policies from HTML reports (F23) - Register script: cross-platform temp path and cleanup of the temporary client secret when certificate upload fails (F30) * Show applications where the checked group is excluded (#126) Get-IntuneGroupAssignment collected exclusion reasons for apps but only added an app to the output when an inclusion assignment supplied the intent, so exclusion-only apps were silently dropped. The intent of the excluding assignment is now used as a fallback. Compare-IntuneGroupAssignment now marks excluded apps with the [EXCLUDED] suffix used by its other sections. Verified against live tenant data: a group excluded from a required app now appears under Required Apps with reason Group Exclusion. * Align README and app registration script with v4.x behavior (#124) - Replace stale auto-update claim with the actual PSGallery version check - Document -AccessToken (Option 4: pre-fetched token) and -ClientSecretCredential auth paths - Options 12/13 and cmdlet reference now say user and/or device; add -DeviceNames examples - Register script grants all 9 validated permissions (GUIDs verified against the Graph service principal) plus documented DeviceManagementServiceConfig.Read.All - Register script next-steps output uses the module cmdlets instead of the removed v3 script * Add shared category-scan engine, category registry, and app protection URI helper (#123) Foundations for decomposing the ten near-identical category-walk cmdlets: - Get-IntuneCategoryDefinition: per-audience category registry; progress totals derive from the list, audience-specific settings catalog filters preserved verbatim - Invoke-IntuneCategoryScan: shared fetch/error/progress engine; per-category failures continue the run and populate the error stream; entity sets cached once per run including the endpoint security intents fallback (retained deliberately: live verification found assigned macOS intents with no configurationPolicies counterpart) - Get-AppProtectionAssignmentUri: single home for the per-type assignments URI switch, now also used by Get-IntuneAssignments - 27 new Pester tests covering caching, dedup, error contracts, and label duality * Migrate Get-IntuneAllPolicies to the shared category-scan engine (#123) 613 -> 144 lines. Adds Add-CategoryExportData, the registry-driven export helper. Legacy output quirks reproduced verbatim (ES intent wording, Autopilot/ESP summary format, App Protection All Devices drop). Entity sets now fetched once per run instead of up to 7 times. * Migrate Search-IntunePolicy to the shared category-scan engine (#123) 544 -> 285 lines. Name matching runs as an entity prefilter so assignment fetches still happen only for matched policies (mock-verified: identical fetch sets old vs new). Console and CSV output byte-identical on fixtures. * Migrate Get-IntuneAllUsersAssignment to the shared category-scan engine (#123) 699 -> 188 lines. All Users predicate via Get-AllTargetReason preserved, including single-intent app copy and ES-in-Settings-Catalog duplication. CSV output byte-identical on fixtures; progress lines now numbered. * Migrate Get-IntuneAllDevicesAssignment to the shared category-scan engine (#123) 682 -> 158 lines. All Devices predicate preserved including first-match intent for apps and legacy double-listing of ES policies under Settings Catalog. CSV byte-identical on fixtures. * Migrate Get-IntuneGroupAssignment to the shared category-scan engine (#123) 688 -> 257 lines. Adds Show-CategoryResultTable, the shared table renderer with the legacy color rules. The excluded-apps behavior from #126 is preserved and re-verified against captured live tenant payloads (identical CSV before and after migration). 13 new tests. * Migrate Get-IntuneUserAssignment to the shared category-scan engine (#123) 1440 -> 381 lines. Entity sets now fetched once per run instead of per user (configurationPolicies: 14 fetches -> 1 for two users). App Protection membership filtering (F13), winning-assignment intent (F14), and excluded-app display preserved and regression-tested. Export rows byte-identical on fixtures. 9 new tests. * Migrate Get-IntuneDeviceAssignment to the shared category-scan engine (#123) 1015 -> 395 lines. Platform compatibility filtering, Windows-conditional Autopilot/ESP/Windows 365 fetching, F14 intent selection, and excluded-app display all preserved and fixture-verified (byte-identical output for Windows and macOS devices). Entity fetches now once per run. Also fixes multi-device input, which was broken: the local split result was assigned into the type-constrained DeviceNames parameter variable, collapsing 'PC-1,PC-2' into a single bogus lookup. 12 new tests. * Migrate Compare-IntuneGroupAssignment to the shared category-scan engine (#123) 699 -> 435 lines. Legacy semantics preserved exactly: intents-only endpoint security scope (prefiltered), per-assignment app entries with EXCLUDED/INHERITED/filter suffixes, shell scripts kept as a local fetch for their divergent groupAssignments endpoint. CSV and matrix output byte-identical on fixtures; Graph calls 58 -> 40 for a two-group run. 15 new tests. * Migrate Test-IntuneGroupMembership to the shared category-scan engine (#123) 1035 -> 449 lines. Simulated-membership delta logic unchanged; user-only, device-only, combined, and scope-tag runs all byte-identical on fixtures. Entity sets fetched once per run (configurationPolicies 7 -> 1). 12 new tests. * Migrate Test-IntuneGroupRemoval to the shared category-scan engine (#123) 1040 -> 480 lines. Removal-simulation delta logic unchanged; user, device, and combined runs byte-identical on fixtures. Entity fetches drop from 46 to 35 Graph calls in the smoke run. 18 new tests. * Use explicit script scope for all remaining GraphEndpoint reads (#123) Sweeps the non-migrated files (Get-IntuneEmptyGroup, Get-IntuneUnassignedPolicy, Get-IntuneUserDeviceAssignment, Test cmdlet group lookup, html-export) so no bare GraphEndpoint interpolation remains anywhere in the module. * Bump module version to 4.3.0 with release notes * Apply engine-era conventions to the non-migrated cmdlets (#123) Get-IntuneUnassignedPolicy, Get-IntuneUserDeviceAssignment, and html-export.ps1 now use Get-AppProtectionAssignmentUri, List-based pagination, and protected category fetches that continue on failure. Get-IntuneUserDeviceAssignment fetches configurationPolicies and intents once per run instead of 13 times.
1 parent a2f2c15 commit b787805

69 files changed

Lines changed: 4865 additions & 6858 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Module/IntuneAssignmentChecker/IntuneAssignmentChecker.psd1

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@{
22
RootModule = 'IntuneAssignmentChecker.psm1'
3-
ModuleVersion = '4.2.0'
3+
ModuleVersion = '4.3.0'
44
GUID = 'c6e25ec6-5787-45ef-95af-8abeb8a17daf'
55
Author = 'Ugur Koc'
66
CompanyName = 'Community'
@@ -43,6 +43,15 @@
4343
ProjectUri = 'https://github.com/ugurkocde/IntuneAssignmentChecker'
4444
IconUri = ''
4545
ReleaseNotes = @'
46+
Version 4.3.0:
47+
- Show applications where the checked group is excluded in Get-IntuneGroupAssignment; Compare-IntuneGroupAssignment now marks excluded apps with [EXCLUDED] (issue #126).
48+
- 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).
49+
- Fix App Protection policies showing for every user regardless of group membership, app intent misclassification, broken multi-device input, junk rows in CSV exports, missing pagination past 100 items in group memberships / comparisons / assignment failures, and Get-IntuneEmptyGroup categories that were displayed but never checked.
50+
- Security hardening: HTML-encode report values, escape OData group-name filters, URL-encode guest UPNs, add -ClientSecretCredential (PSCredential) as the preferred client secret input.
51+
- Fix EDR policies missing from HTML reports due to a template mapping typo.
52+
- Switch-Tenant now connects with the correct permission scopes and refreshes cached filter and group lookups.
53+
- Register-IntuneAssignmentCheckerApp grants all documented permissions, uses a cross-platform temp path, and cleans up its temporary client secret on failure (issue #124).
54+
4655
Version 4.2.0:
4756
- Add -AccessToken (SecureString) parameter for non-interactive authentication using a pre-fetched Microsoft Graph token (Azure Automation managed identities, Azure Functions, federated credentials, parent-script Connect-MgGraph sessions).
4857
- Extend Test-IntuneGroupMembership and Test-IntuneGroupRemoval to accept a Device in addition to a User. The simulation now unions user-side and device-side group memberships.

Module/IntuneAssignmentChecker/IntuneAssignmentChecker.psm1

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ $script:IntentTemplateSubtypeToFamily = @{
1818
'accountProtection' = 'endpointSecurityAccountProtection'
1919
}
2020

21+
# Required Microsoft Graph permissions (shared by Connect-IntuneAssignmentChecker and Switch-Tenant)
22+
$script:RequiredPermissions = @(
23+
@{ Permission = "User.Read.All"; Reason = "Required to read user profile information and check group memberships" }
24+
@{ Permission = "Group.Read.All"; Reason = "Needed to read group information and memberships" }
25+
@{ Permission = "DeviceManagementConfiguration.Read.All"; Reason = "Allows reading Intune device configuration policies and their assignments" }
26+
@{ Permission = "DeviceManagementApps.Read.All"; Reason = "Necessary to read mobile app management policies and app configurations" }
27+
@{ Permission = "DeviceManagementManagedDevices.Read.All"; Reason = "Required to read managed device information and compliance policies" }
28+
@{ Permission = "Device.Read.All"; Reason = "Needed to read device information from Entra ID" }
29+
@{ Permission = "DeviceManagementScripts.Read.All"; Reason = "Needed to read device management and health scripts" }
30+
@{ Permission = "CloudPC.Read.All"; Reason = "Required to read Windows 365 Cloud PC provisioning policies and settings (optional if W365 not licensed)" }
31+
@{ Permission = "DeviceManagementRBAC.Read.All"; Reason = "Required to read role scope tags for scope tag display and filtering" }
32+
)
33+
2134
# Dot-source all private functions
2235
$Private = @(Get-ChildItem -Path "$PSScriptRoot/Private/*.ps1" -ErrorAction SilentlyContinue)
2336
foreach ($file in $Private) {

Module/IntuneAssignmentChecker/Private/Add-AppExportData.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
function Add-AppExportData {
2+
[CmdletBinding()]
23
param (
34
[System.Collections.ArrayList]$ExportData,
45
[string]$Category,
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
function Add-CategoryExportData {
2+
[CmdletBinding()]
3+
param (
4+
[Parameter(Mandatory = $true)]
5+
[AllowEmptyCollection()]
6+
[System.Collections.ArrayList]$ExportData,
7+
8+
# Category registry entries from Get-IntuneCategoryDefinition; export rows are
9+
# emitted in registry order so CSV output matches the pre-refactor sequence.
10+
[Parameter(Mandatory = $true)]
11+
[object[]]$Categories,
12+
13+
# Bucket hashtable as returned by Invoke-IntuneCategoryScan (possibly filtered).
14+
[Parameter(Mandatory = $true)]
15+
[hashtable]$Buckets,
16+
17+
# Passed through to Add-ExportData when provided (string or scriptblock).
18+
[Parameter(Mandatory = $false)]
19+
[object]$AssignmentReason
20+
)
21+
22+
# Categories may share a bucket (e.g. Compare's ShellScripts feed PlatformScripts);
23+
# export each bucket only once, under the first category that declares it.
24+
$exportedBucketKeys = [System.Collections.Generic.HashSet[string]]::new()
25+
26+
foreach ($category in $Categories) {
27+
foreach ($bucketKey in $category.BucketKeys) {
28+
if (-not $exportedBucketKeys.Add($bucketKey)) { continue }
29+
30+
$label = if ($category.BucketExportCategories) {
31+
$category.BucketExportCategories[$bucketKey]
32+
}
33+
else {
34+
$category.ExportCategory
35+
}
36+
if (-not $label) { continue }
37+
38+
$items = if ($Buckets.ContainsKey($bucketKey)) { @($Buckets[$bucketKey]) } else { @() }
39+
40+
# Empty buckets still go through Add-ExportData as a no-op, matching the
41+
# unconditional per-category calls the cmdlets made before the migration.
42+
$addParams = @{
43+
ExportData = $ExportData
44+
Category = $label
45+
Items = $items
46+
}
47+
if ($PSBoundParameters.ContainsKey('AssignmentReason')) {
48+
$addParams.AssignmentReason = $AssignmentReason
49+
}
50+
Add-ExportData @addParams
51+
}
52+
}
53+
}

Module/IntuneAssignmentChecker/Private/Add-ExportData.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
function Add-ExportData {
2+
[CmdletBinding()]
23
param (
34
[System.Collections.ArrayList]$ExportData,
45
[string]$Category,

Module/IntuneAssignmentChecker/Private/Add-IntentTemplateFamilyInfo.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
function Add-IntentTemplateFamilyInfo {
2+
[CmdletBinding()]
23
param (
34
[Parameter(Mandatory = $false)]
45
$IntentPolicies

Module/IntuneAssignmentChecker/Private/Export-PolicyData.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
function Export-PolicyData {
2+
[CmdletBinding()]
23
param (
34
[Parameter(Mandatory = $true)]
45
[System.Collections.ArrayList]$ExportData,

Module/IntuneAssignmentChecker/Private/Export-ResultsIfRequested.ps1

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
function Export-ResultsIfRequested {
2+
[CmdletBinding()]
23
param (
34
[System.Collections.ArrayList]$ExportData,
45
[string]$DefaultFileName,
56
[switch]$ForceExport,
6-
[string]$CustomExportPath
7+
[string]$CustomExportPath,
8+
[switch]$ExportToCSV,
9+
[switch]$ParameterMode
710
)
811

912
if ($ForceExport -or $ExportToCSV) {
@@ -18,7 +21,7 @@ function Export-ResultsIfRequested {
1821
Export-PolicyData -ExportData $ExportData -FilePath $exportPath
1922
}
2023
}
21-
elseif (-not $parameterMode) {
24+
elseif (-not $ParameterMode) {
2225
$export = Read-Host "`nWould you like to export the results to CSV? (y/n)"
2326
if ($export -match '^[Yy]') {
2427
$exportPath = Show-SaveFileDialog -DefaultFileName $DefaultFileName

Module/IntuneAssignmentChecker/Private/Filter-ByScopeTag.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
function Filter-ByScopeTag {
2+
[CmdletBinding()]
23
param (
34
[object[]]$Items,
45
[string]$FilterTag,

Module/IntuneAssignmentChecker/Private/Format-AssignmentFilter.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
function Format-AssignmentFilter {
2+
[CmdletBinding()]
23
param(
34
[Parameter(Mandatory = $false)]
45
[AllowNull()]

0 commit comments

Comments
 (0)