Public/Get-IntuneEnrollmentFlowsReport.ps1
|
<# .SYNOPSIS Generates Intune assignment overview and/or device visualization report. .DESCRIPTION Two modes: (1) Assignment overview only (no device). (2) Device visualization (device required): outputs Assignment Overview tab and Diagram tab (flow with policy details). Device is mandatory for the visualization flow so filters can be validated. Connect first with Connect-RKGraph; this cmdlet uses the existing connection. .PARAMETER AssignmentOverviewOnly Run assignment collection only and generate the same HTML as the standalone assignment overview (no device). .PARAMETER Device Device identifier: display name, Intune managed device ID (GUID), or Entra device object ID (GUID). .PARAMETER MermaidOverview When using `-Device, also export the mermaid diagram to a standalone .mmd file. .PARAMETER ApplyPlatformFilter When using `-Device, exclude policies whose platform does not match the device. Default is off to match legacy report behavior. #> function Get-IntuneEnrollmentFlowsReport { [CmdletBinding(DefaultParameterSetName = 'Device')] param( [Parameter(Mandatory = $false)] [string] $OutputPath, [Parameter(Mandatory = $false)] [switch] $ExportToCsv, [Parameter(Mandatory = $false)] [string] $ExportFolder = '', [Parameter(Mandatory = $true, ParameterSetName = 'AssignmentOnly')] [switch] $AssignmentOverviewOnly, [Parameter(Mandatory = $false, ParameterSetName = 'Device')] [ValidateNotNullOrEmpty()] [string] $Device, [Parameter(Mandatory = $false, ParameterSetName = 'Device')] [switch] $MermaidOverview, [Parameter(Mandatory = $false, ParameterSetName = 'Device')] [switch] $ApplyPlatformFilter, [Parameter(Mandatory = $false)] [switch] $DebugMode ) $ErrorActionPreference = 'Stop' try { if (-not $AssignmentOverviewOnly -and [string]::IsNullOrWhiteSpace($Device)) { Write-Error "You must specify either -AssignmentOverviewOnly (for assignment overview only) or -Device (for device visualization). Example: Get-IntuneEnrollmentFlowsReport -AssignmentOverviewOnly" return } $ctx = Get-MgContext -ErrorAction SilentlyContinue if (-not $ctx) { throw 'Not connected to Microsoft Graph. Run Connect-RKGraph first.' } $tenantInfo = Invoke-MgGraphRequest -Uri 'beta/organization' -Method Get -OutputType PSObject $tenantName = $tenantInfo.value[0].displayName Write-Host "Intune Assignment Overview ($tenantName)" -ForegroundColor White Write-Host '' Write-Host "Connected to tenant: $tenantName" -ForegroundColor Green Write-Host '' if ($AssignmentOverviewOnly) { $assignments = Get-AllIntunePoliciesWithAssignments -DebugMode:$DebugMode Write-Host " Collected $($assignments.Count) assignment records" -ForegroundColor Green Write-Host '' if ($ExportToCsv) { Write-Host 'Exporting to CSV...' -ForegroundColor Yellow $csvPath = Export-Results -Results $assignments -FileName 'IntuneAssignmentOverview' -Extension 'csv' -OutputFolder $ExportFolder -IncludeTimestamp $true -DebugMode:$DebugMode Write-Host "CSV saved: $csvPath" -ForegroundColor Green } $outPath = if ($OutputPath) { if (-not [System.IO.Path]::IsPathRooted($OutputPath)) { Join-Path (Get-Location) $OutputPath } else { $OutputPath } } else { Join-Path (Get-Location) "IntuneAssignmentOverview_$(Get-Date -Format 'yyyyMMdd_HHmmss').html" } $generated = New-AssignmentOverviewHtmlReport -PolicyAssignments $assignments -TenantName $tenantName -OutputPath $outPath Write-Host "Assignment overview report saved: $generated" -ForegroundColor Green try { if ($IsWindows) { Invoke-Item -LiteralPath $generated } else { & /usr/bin/open -- $generated } } catch { Write-Host "Report saved at: $generated" -ForegroundColor Green } Write-Host 'Done.' -ForegroundColor Green return } Write-Host "Resolving device: $Device..." -ForegroundColor Yellow $deviceContext = Get-DeviceEvaluationContext -DeviceNameOrId $Device -DebugMode:$DebugMode if (-not $deviceContext) { Write-Error "Device '$Device' not found in Intune. Script stopped." return } $deviceDisplayName = $deviceContext.DeviceProperties.DeviceName Write-Host " Device found: $deviceDisplayName" -ForegroundColor Green Write-Host '' Write-Host 'Collecting policy assignments...' -ForegroundColor Yellow $assignments = Get-AllIntunePoliciesWithAssignments -DebugMode:$DebugMode Write-Host " Collected $($assignments.Count) assignment records" -ForegroundColor Green Write-Host '' Write-Host 'Evaluating assignments for device...' -ForegroundColor Yellow $evaluatedAssignments = Invoke-EvaluateAssignmentsForDevice -PolicyAssignments $assignments -DeviceContext $deviceContext -ApplyPlatformFilter:$ApplyPlatformFilter -DebugMode:$DebugMode Write-Host ' Evaluated assignments' -ForegroundColor Green $devicePlatform = Get-NormalizedDevicePlatform -OperatingSystem $deviceContext.DeviceProperties.OperatingSystem $modelStr = if ($deviceContext.DeviceProperties.Model) { [string]$deviceContext.DeviceProperties.Model } else { '' } $isCloudPC = $modelStr.Trim().ToLowerInvariant().StartsWith('cloud pc') Write-Host 'Building assignment overview...' -ForegroundColor Yellow $overviewFragment = Get-AssignmentOverviewTabFragment -PolicyAssignments $assignments -TenantName $tenantName Write-Host ' Assignment overview complete' -ForegroundColor Green $deviceGroupDetails = @() $directIds = if ($deviceContext.DeviceDirectGroupIds) { @($deviceContext.DeviceDirectGroupIds) } else { @() } $allGroupIds = if ($deviceContext.DeviceGroupIds) { @($deviceContext.DeviceGroupIds) } else { @() } $allGroupIdsStr = $allGroupIds | ForEach-Object { [string]$_ } $idsToHide = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) foreach ($gid in $allGroupIds) { $memberGroupIds = Get-GroupDirectMemberGroupIds -GroupId $gid foreach ($mid in $memberGroupIds) { [void]$idsToHide.Add([string]$mid) } } $groupIdsToShow = $allGroupIds | Where-Object { -not $idsToHide.Contains([string]$_) } if ($groupIdsToShow -and $groupIdsToShow.Count -gt 0 -and $script:AllGroups.Count -gt 0) { foreach ($gid in $groupIdsToShow) { $g = $script:AllGroups[$gid] if (-not $g -or -not $g.displayName) { continue } $isDynamic = $g.groupTypes -and ($g.groupTypes -contains 'DynamicMembership') $isDirect = ($directIds | ForEach-Object { [string]$_ }) -contains [string]$gid # Validate dynamic group membership: check if device actually matches the rule if ($isDynamic -and $g.membershipRule -and $deviceContext.DeviceProperties) { $rule = $g.membershipRule # Check for [OrderID]:xxx pattern which uses physicalIds if ($rule -match '\[OrderID\]:(\w+)') { $orderIdTag = "[OrderID]:$($matches[1])" $physicalIds = $deviceContext.DeviceProperties.PhysicalIds $hasTag = $false if ($physicalIds) { $hasTag = @($physicalIds) -contains $orderIdTag } # Skip this group if the device doesn't have the required OrderID tag if (-not $hasTag) { Write-Host " ℹ️ Skipping group '$($g.displayName)' - device doesn't match membership rule (missing $orderIdTag)" -ForegroundColor DarkYellow continue } } } $groupTypeLabel = if ($isDynamic) { 'Dynamic' } else { 'Assigned' } if (-not $isDirect) { $groupTypeLabel += ' (nested)' } $ruleDisplay = $null if ($isDirect) { if ($isDynamic -and $g.membershipRule) { $ruleDisplay = [System.Net.WebUtility]::HtmlEncode($g.membershipRule) } else { $parentNames = Get-GroupParentGroupNames -GroupId $gid if ($parentNames -and $parentNames.Count -gt 0) { $nestedValue = ($parentNames | ForEach-Object { '<code>' + [System.Net.WebUtility]::HtmlEncode($_) + '</code>' }) -join '<br>' $ruleDisplay = "<span class=`"arch-rule-inner`"><span class=`"arch-rule-label`">Nested groups:</span><span class=`"arch-rule-value`">$nestedValue</span></span>" } } } else { $nestedFromNames = Get-NestedGroupChainNames -GroupId $gid -DeviceGroupIdsStr $allGroupIdsStr if ($nestedFromNames -and $nestedFromNames.Count -gt 0) { $nestedValue = ($nestedFromNames | ForEach-Object { '<code>' + [System.Net.WebUtility]::HtmlEncode($_) + '</code>' }) -join '<br>' $ruleDisplay = "<span class=`"arch-rule-inner`"><span class=`"arch-rule-label`">Nested groups:</span><span class=`"arch-rule-value`">$nestedValue</span></span>" } } $deviceGroupDetails += [PSCustomObject]@{ DisplayName = $g.displayName GroupType = $groupTypeLabel MembershipRule = $ruleDisplay } } } $outPath = if ($OutputPath) { if (-not [System.IO.Path]::IsPathRooted($OutputPath)) { Join-Path (Get-Location) $OutputPath } else { $OutputPath } } else { Join-Path (Get-Location) "IntuneDeviceVisualization_$(Get-Date -Format 'yyyyMMdd_HHmmss').html" } $intuneId = if ($deviceContext.ManagedDeviceId) { [string]$deviceContext.ManagedDeviceId } else { '' } $entraId = if ($deviceContext.EntraDeviceObjectId) { [string]$deviceContext.EntraDeviceObjectId } else { '' } $deviceGroupIdStrs = @($deviceContext.DeviceGroupIds | ForEach-Object { [string]$_ }) $userGroupIdStrs = @($deviceContext.UserGroupIds | ForEach-Object { [string]$_ }) $generated = New-DeviceVisualizationHtmlReport -EvaluatedAssignments $evaluatedAssignments -DeviceName $deviceDisplayName -TenantName $tenantName -OutputPath $outPath -AssignmentOverviewFragment $overviewFragment -DeviceGroupDetails $deviceGroupDetails -IntuneDeviceId $intuneId -EntraDeviceId $entraId -DevicePlatform $devicePlatform -IsCloudPC:$isCloudPC -DeviceGroupIds $deviceGroupIdStrs -UserGroupIds $userGroupIdStrs Write-Host "Device visualization report saved: $generated" -ForegroundColor Green if ($MermaidOverview) { $mmdPath = [System.IO.Path]::ChangeExtension($generated, '.mmd') [System.IO.File]::WriteAllText($mmdPath, $mermaidDiagram, [System.Text.UTF8Encoding]::new($false)) Write-Host "Mermaid file saved: $mmdPath" -ForegroundColor Green } if ($ExportToCsv) { $csvPath = Export-Results -Results $evaluatedAssignments -FileName 'IntuneDeviceAssignments' -Extension 'csv' -OutputFolder $ExportFolder -IncludeTimestamp $true -DebugMode:$DebugMode Write-Host "CSV saved: $csvPath" -ForegroundColor Green } try { if ($IsWindows) { Invoke-Item -LiteralPath $generated } else { & /usr/bin/open -- $generated } } catch { Write-Host "Report saved at: $generated" -ForegroundColor Green } Write-Host 'Done.' -ForegroundColor Green } catch { Write-Error "Error: $_"; throw $_ } finally { # Session left connected; use Disconnect-RKGraph when done. } } |