Src/Private/Assignment-Helpers.ps1
|
#region --- Assignment Resolution Helpers --- # Used across all section functions to resolve: # - Group IDs -> display names (cached) # - Included vs excluded group assignments # - Scope tag IDs -> names (cached) # - Empty group detection (member count = 0) # All lookups are initialised once at report startup and stored in script-scoped hashtables. #region --- Initialise Lookup Tables --- function Initialize-IntuneGroupLookup { <# .SYNOPSIS Pre-fetches all Azure AD groups referenced in Intune assignments and builds an ID -> DisplayName hashtable. Called once at report startup. Stores results in $script:GroupNameCache. #> [CmdletBinding()] param() $script:GroupNameCache = @{} $script:GroupMemberCache = @{} # groupId -> member count (-1 = not fetched) Write-AbrDebugLog 'Group lookup cache initialised (lazy-populated on first use)' 'INFO' 'GROUPS' } function Initialize-IntuneScopeTagLookup { <# .SYNOPSIS Fetches all Intune Role Scope Tags and builds an ID -> name hashtable. Stored in $script:ScopeTagCache. Called once at report startup. #> [CmdletBinding()] param() $script:ScopeTagCache = @{ '0' = 'Default' } try { $resp = Invoke-MgGraphRequest -Method GET ` -Uri "$($script:GraphEndpoint)/beta/deviceManagement/roleScopeTags?`$select=id,displayName" ` -ErrorAction SilentlyContinue if ($resp.value) { foreach ($tag in $resp.value) { $script:ScopeTagCache["$($tag.id)"] = $tag.displayName } } Write-Host " - Scope tag lookup: $($script:ScopeTagCache.Count) tag(s) loaded" -ForegroundColor Cyan Write-AbrDebugLog "Scope tag lookup: $($script:ScopeTagCache.Count) tags" 'INFO' 'SCOPETAGS' } catch { Write-AbrDebugLog "Scope tag lookup failed: $($_.Exception.Message)" 'WARN' 'SCOPETAGS' } } #endregion #region --- Group Name Resolution --- function Get-IntuneGroupName { <# .SYNOPSIS Returns the display name for an Azure AD group ID. Uses $script:GroupNameCache to avoid repeated Graph calls. Falls back to a truncated ID if the group cannot be resolved. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [string]$GroupId ) if (-not $GroupId) { return '--' } # Return from cache if available if ($script:GroupNameCache.ContainsKey($GroupId)) { return $script:GroupNameCache[$GroupId] } # Fetch from Graph try { $resp = Invoke-MgGraphRequest -Method GET ` -Uri "$($script:GraphEndpoint)/v1.0/groups/${GroupId}?`$select=id,displayName" ` -ErrorAction Stop $name = if ($resp.displayName) { $resp.displayName } else { "Unknown ($GroupId)" } $script:GroupNameCache[$GroupId] = $name return $name } catch { $short = if ($GroupId.Length -gt 8) { "$($GroupId.Substring(0,8))..." } else { $GroupId } $script:GroupNameCache[$GroupId] = "Unknown ($short)" return $script:GroupNameCache[$GroupId] } } function Get-IntuneGroupMemberCount { <# .SYNOPSIS Returns the member count for an Azure AD group. Returns -1 if unavailable, 0 if empty (triggers HealthCheck Warning). Uses $script:GroupMemberCache to avoid repeated calls. #> [CmdletBinding()] [OutputType([int])] param( [Parameter(Mandatory)] [string]$GroupId ) if (-not $GroupId) { return -1 } if ($script:GroupMemberCache.ContainsKey($GroupId)) { return $script:GroupMemberCache[$GroupId] } try { $resp = Invoke-MgGraphRequest -Method GET ` -Uri "$($script:GraphEndpoint)/v1.0/groups/$GroupId/members/`$count" ` -Headers @{ 'ConsistencyLevel' = 'eventual' } ` -ErrorAction Stop $count = [int]$resp $script:GroupMemberCache[$GroupId] = $count return $count } catch { $script:GroupMemberCache[$GroupId] = -1 return -1 } } function Resolve-IntuneAssignments { <# .SYNOPSIS Resolves an array of Intune assignment objects into human-readable strings. Returns a [pscustomobject] with: - IncludedGroups : comma-separated group display names (included targets) - ExcludedGroups : comma-separated group display names (excluded targets) - AllUsers : $true if All Users virtual group is targeted - AllDevices : $true if All Devices virtual group is targeted - HasEmptyGroup : $true if any included group has 0 members - AssignmentSummary : short single string for summary tables .PARAMETER Assignments Array of assignment objects from a Graph API response (with .target property). .PARAMETER CheckMemberCount If $true, fetches member counts for each group (slower but detects empty groups). #> [CmdletBinding()] param( [Parameter()] [object[]]$Assignments, [switch]$CheckMemberCount ) $includedNames = [System.Collections.ArrayList]::new() $excludedNames = [System.Collections.ArrayList]::new() $allUsers = $false $allDevices = $false $hasEmptyGroup = $false if (-not $Assignments -or $Assignments.Count -eq 0) { return [pscustomobject]@{ IncludedGroups = '--' ExcludedGroups = '--' AllUsers = $false AllDevices = $false HasEmptyGroup = $false AssignmentSummary = 'Not assigned' } } foreach ($a in $Assignments) { $target = $a.target $odataType = $target.'@odata.type' switch ($odataType) { '#microsoft.graph.allLicensedUsersAssignmentTarget' { $allUsers = $true } '#microsoft.graph.allDevicesAssignmentTarget' { $allDevices = $true } '#microsoft.graph.groupAssignmentTarget' { $groupId = $target.groupId $groupName = Get-IntuneGroupName -GroupId $groupId $null = $includedNames.Add($groupName) if ($CheckMemberCount) { $count = Get-IntuneGroupMemberCount -GroupId $groupId if ($count -eq 0) { $hasEmptyGroup = $true } } } '#microsoft.graph.exclusionGroupAssignmentTarget' { $groupId = $target.groupId $groupName = Get-IntuneGroupName -GroupId $groupId $null = $excludedNames.Add("[EXCLUDED] $groupName") } } } # Build summary string $parts = [System.Collections.ArrayList]::new() if ($allUsers) { $null = $parts.Add('All Users') } if ($allDevices) { $null = $parts.Add('All Devices') } if ($includedNames.Count -gt 0) { $null = $parts.Add($includedNames -join ', ') } $summary = if ($parts.Count -gt 0) { $parts -join ' | ' } else { 'Not assigned' } $includedStr = if ($allUsers) { 'All Users' } elseif ($allDevices) { 'All Devices' } elseif ($includedNames.Count -gt 0) { $includedNames -join ', ' } else { '--' } if ($allUsers -and $includedNames.Count -gt 0) { $includedStr = "All Users, $($includedNames -join ', ')" } elseif ($allDevices -and $includedNames.Count -gt 0) { $includedStr = "All Devices, $($includedNames -join ', ')" } $excludedStr = if ($excludedNames.Count -gt 0) { $excludedNames -join ', ' } else { '--' } return [pscustomobject]@{ IncludedGroups = $includedStr ExcludedGroups = $excludedStr AllUsers = $allUsers AllDevices = $allDevices HasEmptyGroup = $hasEmptyGroup AssignmentSummary = $summary } } #endregion #region --- Scope Tag Resolution --- function Get-IntuneScopeTagNames { <# .SYNOPSIS Resolves an array of scope tag IDs to display names using the cached lookup. Returns a comma-separated string, e.g. "Default, APAC-Region, IT-Helpdesk" #> [CmdletBinding()] [OutputType([string])] param( [Parameter()] [object[]]$ScopeTagIds ) if (-not $ScopeTagIds -or $ScopeTagIds.Count -eq 0) { return 'Default' } $names = $ScopeTagIds | ForEach-Object { $id = "$_" if ($script:ScopeTagCache -and $script:ScopeTagCache.ContainsKey($id)) { $script:ScopeTagCache[$id] } else { "Tag-$id" } } return $names -join ', ' } #endregion #endregion |