backup/Build-AssignmentReport.ps1
|
#Requires -Version 7.0 <# .SYNOPSIS Generates a group-centric assignment report across all Intune policy categories. .DESCRIPTION Fetches every policy / app / script in the tenant, collects their assignments, inverts the structure so the output is keyed by Azure AD group, and writes the result to "Assignment Report\report.json" inside BackupPath. The report shows, for each group: - groupName : Azure AD display name - groupType : DynamicMembership | StaticMembership - membershipRule : dynamic rule string (null for static groups) - assignedTo : object whose keys are Intune category names, each containing an array of { name, type, intent } objects .PARAMETER BackupPath The output root directory (same as the rest of the backup). .PARAMETER Token Bearer token for Microsoft Graph as a SecureString. .PARAMETER ScopeTagMap Unused by this function; present to match the standard backup module signature. .EXAMPLE Build-AssignmentReport -BackupPath C:\Backup -Token $token #> function Build-AssignmentReport { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$BackupPath, [Parameter(Mandatory)] [SecureString]$Token, [hashtable]$ScopeTagMap = @{} ) try { $folder = Join-Path $BackupPath 'Assignment Report' # --------------------------------------------------------------------------- # Category definitions # Label : key used in the assignedTo object # ListUri : Graph endpoint to enumerate items # AssignBase : /beta/<AssignBase>/{id}/assignments — $null for app-protection # IntentMode : 'field' = read from assignment.intent # 'apply' = hardcode "apply" # 'empty' = "" # TypeFromOdata : $true = strip '#microsoft.graph.' from @odata.type # $false = always "" # Multiple rows may share the same Label; items accumulate in the same bucket. # --------------------------------------------------------------------------- $categories = @( @{ Label = 'Proactive Remediations'; ListUri = '/beta/deviceManagement/deviceHealthScripts'; AssignBase = 'deviceManagement/deviceHealthScripts'; IntentMode = 'empty'; TypeFromOdata = $false }, @{ Label = 'Feature Updates'; ListUri = '/beta/deviceManagement/windowsFeatureUpdateProfiles'; AssignBase = 'deviceManagement/windowsFeatureUpdateProfiles'; IntentMode = 'empty'; TypeFromOdata = $false }, @{ Label = 'Quality Updates'; ListUri = '/beta/deviceManagement/windowsQualityUpdateProfiles'; AssignBase = 'deviceManagement/windowsQualityUpdateProfiles'; IntentMode = 'empty'; TypeFromOdata = $false }, @{ Label = 'Driver Updates'; ListUri = '/beta/deviceManagement/windowsDriverUpdateProfiles'; AssignBase = 'deviceManagement/windowsDriverUpdateProfiles'; IntentMode = 'empty'; TypeFromOdata = $false }, @{ Label = 'Settings Catalog'; ListUri = '/beta/deviceManagement/configurationPolicies'; AssignBase = 'deviceManagement/configurationPolicies'; IntentMode = 'empty'; TypeFromOdata = $false }, @{ Label = 'Device Configurations'; ListUri = '/beta/deviceManagement/deviceConfigurations'; AssignBase = 'deviceManagement/deviceConfigurations'; IntentMode = 'apply'; TypeFromOdata = $true }, @{ Label = 'Compliance Policies'; ListUri = '/beta/deviceManagement/compliancePolicies'; AssignBase = 'deviceManagement/compliancePolicies'; IntentMode = 'empty'; TypeFromOdata = $true }, @{ Label = 'Compliance Policies'; ListUri = '/beta/deviceManagement/deviceCompliancePolicies'; AssignBase = 'deviceManagement/deviceCompliancePolicies'; IntentMode = 'empty'; TypeFromOdata = $true }, @{ Label = 'Group Policy Configurations'; ListUri = '/beta/deviceManagement/groupPolicyConfigurations'; AssignBase = 'deviceManagement/groupPolicyConfigurations'; IntentMode = 'empty'; TypeFromOdata = $false }, @{ Label = 'Management Intents'; ListUri = '/beta/deviceManagement/intents'; AssignBase = 'deviceManagement/intents'; IntentMode = 'empty'; TypeFromOdata = $false }, @{ Label = 'Scripts'; ListUri = '/beta/deviceManagement/deviceManagementScripts'; AssignBase = 'deviceManagement/deviceManagementScripts'; IntentMode = 'empty'; TypeFromOdata = $false }, @{ Label = 'Scripts'; ListUri = '/beta/deviceManagement/deviceShellScripts'; AssignBase = 'deviceManagement/deviceShellScripts'; IntentMode = 'empty'; TypeFromOdata = $false }, @{ Label = 'Applications'; ListUri = '/beta/deviceAppManagement/mobileApps'; AssignBase = 'deviceAppManagement/mobileApps'; IntentMode = 'field'; TypeFromOdata = $true }, @{ Label = 'App Protection'; ListUri = '/beta/deviceAppManagement/managedAppPolicies'; AssignBase = $null; IntentMode = 'empty'; TypeFromOdata = $true } ) # Map app-protection @odata.type names to their specific assignments collection $appProtectionMap = @{ 'iosManagedAppProtection' = 'deviceAppManagement/iosManagedAppProtections' 'androidManagedAppProtection' = 'deviceAppManagement/androidManagedAppProtections' 'mdmWindowsInformationProtectionPolicy' = 'deviceAppManagement/mdmWindowsInformationProtectionPolicies' 'windowsManagedAppProtection' = 'deviceAppManagement/windowsManagedAppProtections' 'targetedManagedAppConfiguration' = 'deviceAppManagement/targetedManagedAppConfigurations' } # groupId -> [ordered]@{ groupName; groupType; membershipRule; assignedTo <ordered> } $groupAssignments = [ordered]@{} # groupId -> Graph group object (cached to avoid re-fetching) $groupCache = @{} # ----------------------------------------------------------------------- # Main loop: enumerate every category, then every item, then its assignments # ----------------------------------------------------------------------- foreach ($cat in $categories) { $items = @() try { $fetched = Invoke-GraphRequest2 -Uri $cat.ListUri -Token $Token if ($fetched) { $items = @($fetched) } } catch { Write-Verbose "Assignment report: failed to list '$($cat.Label)' from $($cat.ListUri): $_" continue } foreach ($item in $items) { $itemId = $item.id $itemName = if ($item.displayName) { $item.displayName } elseif ($item.name) { $item.name } else { $itemId } $itemType = '' if ($cat.TypeFromOdata -and $item.'@odata.type') { $itemType = $item.'@odata.type' -replace '#microsoft\.graph\.', '' } # Build the assignments URI if ($null -ne $cat.AssignBase) { $assignUri = "/beta/$($cat.AssignBase)/$itemId/assignments" } else { # App protection: derive collection from @odata.type $typeName = $item.'@odata.type' -replace '#microsoft\.graph\.', '' if (-not $appProtectionMap.ContainsKey($typeName)) { Write-Verbose "Assignment report: unknown app-protection type '$typeName', skipping '$itemName'" continue } $assignUri = "/beta/$($appProtectionMap[$typeName])/$itemId/assignments" } $assignments = @() try { $fetched = Invoke-GraphRequest2 -Uri $assignUri -Token $Token if ($fetched) { $assignments = @($fetched) } } catch { Write-Verbose "Assignment report: failed to fetch assignments for '$itemName': $_" continue } foreach ($assignment in $assignments) { $target = $assignment.target $targetType = $target.'@odata.type' # Only group-based targets (skip allDevices / allLicensedUsers etc.) if ($targetType -notmatch 'groupAssignmentTarget|exclusionGroupAssignmentTarget') { continue } $groupId = $target.groupId if (-not $groupId) { continue } # Resolve + cache group details if (-not $groupCache.ContainsKey($groupId)) { try { $groupCache[$groupId] = Invoke-GraphRequest2 ` -Uri "/beta/groups/$($groupId)?`$select=id,displayName,groupTypes,membershipRule" ` -Token $Token } catch { Write-Verbose "Assignment report: could not resolve group ${groupId}: $_" $groupCache[$groupId] = [PSCustomObject]@{ displayName = $groupId groupTypes = @() membershipRule = $null } } } $grpInfo = $groupCache[$groupId] $grpType = if (@($grpInfo.groupTypes) -contains 'DynamicMembership') { 'DynamicMembership' } else { 'StaticMembership' } if (-not $groupAssignments.ContainsKey($groupId)) { $groupAssignments[$groupId] = [ordered]@{ groupName = $grpInfo.displayName groupType = $grpType membershipRule = $grpInfo.membershipRule assignedTo = [ordered]@{} } } $assignedTo = $groupAssignments[$groupId]['assignedTo'] if (-not $assignedTo.Contains($cat.Label)) { $assignedTo[$cat.Label] = [System.Collections.Generic.List[object]]::new() } $intent = switch ($cat.IntentMode) { 'field' { if ($assignment.intent) { $assignment.intent } else { '' } } 'apply' { 'apply' } default { '' } } # Deduplicate: same item should appear only once per category per group if (-not ($assignedTo[$cat.Label] | Where-Object { $_.name -eq $itemName })) { $assignedTo[$cat.Label].Add([PSCustomObject]@{ name = $itemName type = $itemType intent = $intent }) } } } } if ($groupAssignments.Count -eq 0) { Write-Verbose 'Assignment report: no group assignments found' return } # Build sorted output — convert List values to plain arrays for clean JSON $report = @( $groupAssignments.GetEnumerator() | Sort-Object { $_.Value['groupName'] } | ForEach-Object { $entry = $_.Value $assignedTo = [ordered]@{} foreach ($key in $entry['assignedTo'].Keys) { $assignedTo[$key] = @($entry['assignedTo'][$key]) } [ordered]@{ groupName = $entry['groupName'] groupType = $entry['groupType'] membershipRule = $entry['membershipRule'] assignedTo = $assignedTo } } ) if (!(Test-Path -Path $folder -PathType Container)) { New-Item -ItemType Directory -Path $folder -Force > $null } $filePath = Join-Path $folder 'report.json' $report | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $filePath -Encoding UTF8 -Force Write-Verbose "Assignment report saved to $filePath ($($report.Count) groups)" } catch { Write-Error "Failed to build assignment report: $_" return } } Export-ModuleMember -Function Build-AssignmentReport |