Private/Resolve-InforcerGraphEnrichment.ps1
|
function Resolve-InforcerGraphEnrichment { <# .SYNOPSIS Connects to Microsoft Graph and resolves the IDs in a tenant's policies to display names. .DESCRIPTION Connects to Graph for the tenant's Azure AD (Connect-InforcerGraph may reuse a matching context) and builds fresh maps for this tenant only: GroupNameMap assignment groups (and, with -ConditionalAccess, CA include/exclude groups), via directoryObjects/getByIds (up to 1000 ids per call) FilterMap assignment filters ScopeTagMap Intune scope tags With -ConditionalAccess also RoleNameMap (roleDefinitions: built-in and custom roles), LocationNameMap (named locations) and AppNameMap (servicePrincipals filtered by appId, 15 ids per call; a small static map only names first-party apps the tenant has no service principal for). With -ComplianceRules also ComplianceRulesMap and ComplianceScriptLinkMap (one GET per compliance policy: the list endpoint does not return deviceCompliancePolicyScript). Shared by Export-InforcerTenantDocumentation and Get-InforcerComparisonData. .PARAMETER DocData Hashtable from Get-InforcerDocData containing Tenant and Policies. .PARAMETER Label Display label for progress messages (e.g., 'Source', 'Destination'). .PARAMETER ConditionalAccess Also resolve Conditional Access groups, roles, named locations and applications. .PARAMETER ComplianceRules Also fetch custom compliance rules and linked discovery scripts. .OUTPUTS Hashtable of maps. Empty when the Graph connection fails. #> [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$DocData, [Parameter()] [string]$Label = '', [Parameter()] [switch]$ConditionalAccess, [Parameter()] [switch]$ComplianceRules ) $prefix = if ($Label) { " [$Label] " } else { ' ' } $msTenantId = $null if ($DocData.Tenant -and $DocData.Tenant.PSObject.Properties['msTenantId']) { $msTenantId = $DocData.Tenant.msTenantId } $graphConnectParams = @{} if ($msTenantId) { $graphConnectParams['TenantId'] = $msTenantId } $graphCtx = Connect-InforcerGraph @graphConnectParams if (-not $graphCtx) { Write-Warning "${prefix}Microsoft Graph connection failed. Falling back to raw ObjectIDs." return @{} } Write-Host "${prefix}Graph connected as: $($graphCtx.Account)" -ForegroundColor Green if ($msTenantId -and $graphCtx.TenantId -and $graphCtx.TenantId -ne $msTenantId) { $tenantName = if ($DocData.Tenant.tenantFriendlyName) { $DocData.Tenant.tenantFriendlyName } else { $msTenantId } Write-Warning "${prefix}Graph signed into tenant $($graphCtx.TenantId) but target tenant is '$tenantName' ($msTenantId). Names may not resolve correctly; sign in with an account of that tenant or skip -FetchGraphData." } $guidPattern = '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' $objectIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $roleIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $locationIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $appIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $addIds = { param($Set, $Values) foreach ($v in @($Values)) { if ($v -and "$v" -match $guidPattern) { [void]$Set.Add("$v") } } } foreach ($policy in @($DocData.Policies)) { $rawAssign = $policy.policyData.assignments if ($null -eq $rawAssign) { $rawAssign = $policy.assignments } foreach ($a in @($rawAssign)) { if ($null -eq $a) { continue } $t = $a.target; if ($null -eq $t) { $t = $a } & $addIds $objectIds $t.groupId } if (-not $ConditionalAccess) { continue } $conditions = $policy.policyData.conditions if ($null -eq $conditions) { continue } if ($conditions.users) { & $addIds $objectIds $conditions.users.includeGroups & $addIds $objectIds $conditions.users.excludeGroups & $addIds $roleIds $conditions.users.includeRoles & $addIds $roleIds $conditions.users.excludeRoles } if ($conditions.locations) { & $addIds $locationIds $conditions.locations.includeLocations & $addIds $locationIds $conditions.locations.excludeLocations } if ($conditions.applications) { & $addIds $appIds $conditions.applications.includeApplications & $addIds $appIds $conditions.applications.excludeApplications } } $maps = @{} $groupNameMap = @{} if ($objectIds.Count -gt 0) { Write-Host "${prefix}Resolving $($objectIds.Count) unique group/object IDs..." -ForegroundColor Gray $idList = [string[]]@($objectIds) for ($i = 0; $i -lt $idList.Count; $i += 1000) { $chunk = $idList[$i..([Math]::Min($i + 999, $idList.Count - 1))] $body = @{ ids = $chunk; types = @('group', 'user', 'device', 'servicePrincipal') } | ConvertTo-Json -Depth 100 -Compress $response = Invoke-InforcerGraphRequest -Uri 'https://graph.microsoft.com/v1.0/directoryObjects/getByIds' -Method POST -Body $body -SingleObject foreach ($obj in @($response.value)) { if ($obj.id -and $obj.displayName) { $groupNameMap[$obj.id] = $obj.displayName } } } $resolved = $groupNameMap.Count foreach ($oid in $idList) { if (-not $groupNameMap.ContainsKey($oid)) { $groupNameMap[$oid] = $oid } } Write-Host "${prefix}Resolved $resolved of $($objectIds.Count) group names" -ForegroundColor Gray } $maps['GroupNameMap'] = $groupNameMap Write-Host "${prefix}Fetching assignment filters..." -ForegroundColor Gray $filterMap = @{} foreach ($f in @(Invoke-InforcerGraphRequest -Uri 'https://graph.microsoft.com/beta/deviceManagement/assignmentFilters')) { if ($f.id) { $filterMap[$f.id] = $f } } Write-Host "${prefix}Loaded $($filterMap.Count) assignment filters" -ForegroundColor Gray $maps['FilterMap'] = $filterMap Write-Host "${prefix}Fetching scope tags..." -ForegroundColor Gray $scopeTagMap = @{} foreach ($st in @(Invoke-InforcerGraphRequest -Uri 'https://graph.microsoft.com/beta/deviceManagement/roleScopeTags')) { if ($null -ne $st.id) { $scopeTagMap[$st.id.ToString()] = $st.displayName } } Write-Host "${prefix}Loaded $($scopeTagMap.Count) scope tags" -ForegroundColor Gray $maps['ScopeTagMap'] = $scopeTagMap if ($ConditionalAccess) { $roleNameMap = @{} if ($roleIds.Count -gt 0) { Write-Host "${prefix}Resolving $($roleIds.Count) unique role IDs..." -ForegroundColor Gray foreach ($rd in @(Invoke-InforcerGraphRequest -Uri 'https://graph.microsoft.com/v1.0/roleManagement/directory/roleDefinitions?$select=id,displayName')) { if ($rd.id) { $roleNameMap[$rd.id] = $rd.displayName } } $resolvedRoles = @($roleIds | Where-Object { $roleNameMap.ContainsKey($_) }).Count Write-Host "${prefix}Resolved $resolvedRoles of $($roleIds.Count) role names" -ForegroundColor Gray } $maps['RoleNameMap'] = $roleNameMap $locationNameMap = @{} if ($locationIds.Count -gt 0) { Write-Host "${prefix}Resolving $($locationIds.Count) unique named location IDs..." -ForegroundColor Gray foreach ($nl in @(Invoke-InforcerGraphRequest -Uri 'https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations?$select=id,displayName')) { if ($nl.id) { $locationNameMap[$nl.id] = $nl.displayName } } $resolvedLocations = @($locationIds | Where-Object { $locationNameMap.ContainsKey($_) }).Count Write-Host "${prefix}Resolved $resolvedLocations of $($locationIds.Count) named location names" -ForegroundColor Gray } $maps['LocationNameMap'] = $locationNameMap $appNameMap = @{} if ($appIds.Count -gt 0) { Write-Host "${prefix}Resolving $($appIds.Count) application IDs..." -ForegroundColor Gray $appList = [string[]]@($appIds) for ($i = 0; $i -lt $appList.Count; $i += 15) { $chunk = $appList[$i..([Math]::Min($i + 14, $appList.Count - 1))] $filter = [uri]::EscapeDataString("appId in ('" + ($chunk -join "','") + "')") $uri = "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=$filter&`$select=appId,displayName" foreach ($sp in @(Invoke-InforcerGraphRequest -Uri $uri)) { if ($sp.appId -and $sp.displayName) { $appNameMap[$sp.appId] = $sp.displayName } } } # ponytail: fallback only for first-party apps with no service principal in this tenant; never overrides Graph. $knownApps = @{ '0000000a-0000-0000-c000-000000000000' = 'Microsoft Intune' '00000002-0000-0000-c000-000000000000' = 'Windows Azure Active Directory' '00000002-0000-0ff1-ce00-000000000000' = 'Office 365 Exchange Online' '00000003-0000-0000-c000-000000000000' = 'Microsoft Graph' '00000003-0000-0ff1-ce00-000000000000' = 'Office 365 SharePoint Online' '00000004-0000-0ff1-ce00-000000000000' = 'Skype for Business Online' '0000000c-0000-0000-c000-000000000000' = 'Microsoft App Access Panel' '00000007-0000-0000-c000-000000000000' = 'Dataverse' '00000006-0000-0ff1-ce00-000000000000' = 'Microsoft Office 365 Portal' '00000009-0000-0000-c000-000000000000' = 'Power BI' '00000015-0000-0000-c000-000000000000' = 'Microsoft Dynamics ERP' '01cb2876-7ebd-4aa4-9cc9-d28bd4d359a9' = 'Device Registration Service' 'd4ebce55-015a-49b5-a083-c84d1797ae8c' = 'Microsoft Intune Enrollment' 'fc780465-2017-40d4-a0c5-307022471b92' = 'Windows Sign In' '04b07795-8ddb-461a-bbee-02f9e1bf7b46' = 'Microsoft Azure CLI' '1950a258-227b-4e31-a9cf-717495945fc2' = 'Microsoft Azure PowerShell' 'fb78d390-0c51-40cd-8e17-fdbfab77341b' = 'Microsoft Exchange REST API' 'de8bc8b5-d9f9-48b1-a8ad-b748da725064' = 'Graph Explorer' '1fec8e78-bce4-4aaf-ab1b-5451cc387264' = 'Microsoft Teams' 'cc15fd57-2c6c-4117-a88c-83b1d56b4bbe' = 'Microsoft Teams Services' '27922004-5251-4030-b22d-91ecd9a37ea4' = 'Outlook Mobile' '4765445b-32c6-49b0-83e6-1d93765276ca' = 'OfficeHome' } foreach ($appId in $appList) { if (-not $appNameMap.ContainsKey($appId) -and $knownApps.ContainsKey($appId)) { $appNameMap[$appId] = $knownApps[$appId] } } $resolvedApps = @($appList | Where-Object { $appNameMap.ContainsKey($_) }).Count Write-Host "${prefix}Resolved $resolvedApps of $($appIds.Count) application names" -ForegroundColor Gray } $maps['AppNameMap'] = $appNameMap } if ($ComplianceRules) { Write-Host "${prefix}Fetching compliance rules..." -ForegroundColor Gray $complianceRulesMap = @{} $complianceScriptLinkMap = @{} $compPolicyIds = @($DocData.Policies | Where-Object { $_.policyData -and $_.policyData.id -and $_.policyData.'@odata.type' -match 'CompliancePolicy' } | ForEach-Object { @{ Id = $_.policyData.id; Name = $_.displayName } }) Write-Host "${prefix}Checking $($compPolicyIds.Count) compliance policies for custom rules..." -ForegroundColor Gray foreach ($cpInfo in $compPolicyIds) { try { $cpFull = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/beta/deviceManagement/deviceCompliancePolicies('$($cpInfo.Id)')" ` -Method GET -OutputType PSObject -ErrorAction Stop } catch { Write-Verbose "${prefix} Failed to fetch: $($cpInfo.Name) - $($_.Exception.Message)" continue } $cpScript = $cpFull.deviceCompliancePolicyScript if ($cpScript -and $cpScript.rulesContent) { try { $complianceRulesMap[$cpInfo.Id] = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($cpScript.rulesContent)) Write-Host "${prefix} Compliance rules found: $($cpInfo.Name)" -ForegroundColor Green } catch { $complianceRulesMap[$cpInfo.Id] = $cpScript.rulesContent } } if ($cpScript -and $cpScript.deviceComplianceScriptId) { $complianceScriptLinkMap[$cpInfo.Id] = $cpScript.deviceComplianceScriptId } } Write-Host "${prefix}Found compliance rules for $($complianceRulesMap.Count) of $($compPolicyIds.Count) policies" -ForegroundColor Gray $maps['ComplianceRulesMap'] = $complianceRulesMap $maps['ComplianceScriptLinkMap'] = $complianceScriptLinkMap } $maps } |