Private/AuthContext/Core/Convert-PIMPoliciesToAuthContext.ps1
|
function Convert-PIMPoliciesToAuthContext { <# .SYNOPSIS Converts raw PIM policy objects (directory / group / managed group / Azure) into normalized auth context records. .DESCRIPTION Parses policy rule JSON to extract authenticationContextIds and authenticationContextClassReferences (including singular and include* variants) plus fallback token capture. Adds resolved context names, role definition id, scope metadata and optional original group name. Displays progress (Id 6) unless -NoProgress. .PARAMETER Policies Collection of raw roleManagementPolicy objects (with .rules) potentially annotated with RoleDefinitionId / GroupName. .PARAMETER AuthContexts Collection of authentication contexts used for name resolution (optional but improves output readability). .OUTPUTS PSCustomObject: PolicyId, ScopeId, ScopeType, AuthContextIds, AuthContextClassRefs, AuthContextNames, MatchedContextNamesText, RawContainsAuthContext, RoleDefinitionId, GroupName. .NOTES Intentionally skips policies with no detected auth context tokens to reduce noise. .EXAMPLE $normalized = Convert-PIMPoliciesToAuthContext -Policies $raw -AuthContexts $authContexts #> param([Parameter(Mandatory)][object[]]$Policies, [object[]]$AuthContexts) $contextNames = @(); if ($AuthContexts) { $contextNames = $AuthContexts.DisplayName } $output = @() $totalPolicies = ($Policies | Measure-Object).Count $policyIndex = 0 foreach ($policy in $Policies) { $policyIndex++ $policyPercent = if ($totalPolicies -gt 0) { [int](($policyIndex / $totalPolicies) * 100) } else { 100 } if (-not $NoProgress) { $scopeText = if ($policy.scopeType) { $policy.scopeType } else { 'UnknownScope' } $roleHint = $null if ($policy.PSObject.Properties.Name -contains 'RoleDefinitionId' -and $policy.RoleDefinitionId) { $roleHint = $policy.RoleDefinitionId } $statusLine = "Policy $policyIndex/$totalPolicies ($scopeText)" + $(if ($roleHint) { " :: RoleDef=$roleHint" } else { '' }) Write-Progress -Id 6 -Activity 'PIM Policies' -Status $statusLine -PercentComplete $policyPercent } $rules = $policy.rules; if (-not $rules) { continue } $ruleJson = $rules | ConvertTo-Json -Depth 15 -Compress # Extract Authentication Context IDs and Class References from JSON arrays $contextIds = @() $contextClasses = @() $authContextIdMatches = [regex]::Matches($ruleJson, '"authenticationContextIds"\s*:\s*\[(.*?)\]') foreach ($matchItem in $authContextIdMatches) { $inner = $matchItem.Groups[1].Value $innerIds = [regex]::Matches($inner, '"([0-9a-fA-F-]{36}|c\d+)"') | ForEach-Object { $_.Groups[1].Value } if ($innerIds) { $contextIds += $innerIds } } $authContextClassMatches = [regex]::Matches($ruleJson, '"authenticationContextClassReferences"\s*:\s*\[(.*?)\]') foreach ($matchItem in $authContextClassMatches) { $inner = $matchItem.Groups[1].Value $innerClasses = [regex]::Matches($inner, '"([^"\\]+)"') | ForEach-Object { $_.Groups[1].Value } if ($innerClasses) { $contextClasses += $innerClasses } } # Handle singular variants that sometimes appear in rules if ($ruleJson -match '"authenticationContextId"\s*:\s*"([0-9a-fA-F-]{36}|c\d+)"') { $contextIds += $Matches[1] } if ($ruleJson -match '"authenticationContextClassReference"\s*:\s*"([^"\\]+)"') { $contextClasses += $Matches[1] } # Handle legacy include* property variants $includeClassReferences = [regex]::Matches($ruleJson, '"includeAuthenticationContextClassReferences"\s*:\s*\[(.*?)\]') foreach ($matchItem in $includeClassReferences) { $contextClasses += ([regex]::Matches($matchItem.Groups[1].Value, '"([^"\\]+)"') | ForEach-Object { $_.Groups[1].Value }) } $includeIds = [regex]::Matches($ruleJson, '"includeAuthenticationContextIds"\s*:\s*\[(.*?)\]') foreach ($matchItem in $includeIds) { $contextIds += ([regex]::Matches($matchItem.Groups[1].Value, '"([0-9a-fA-F-]{36}|c\d+)"') | ForEach-Object { $_.Groups[1].Value }) } # Fallback: if rule mentions authenticationContextRule or claimValue, collect cN tokens as class refs if (($contextIds.Count -eq 0 -and $contextClasses.Count -eq 0) -and ($ruleJson -match 'authenticationContextRule' -or $ruleJson -match 'claimValue')) { $classTokens = [regex]::Matches($ruleJson, '"c\d+"') | ForEach-Object { $_.Value.Trim('"') } if ($classTokens) { $contextClasses += $classTokens } } # Only include policies when we have explicit Authentication Context arrays to avoid false positives if ($contextIds.Count -eq 0 -and $contextClasses.Count -eq 0) { continue } $matchedNames = @() if ($contextNames) { $matchedNames = @($contextNames | Where-Object { $_ -and ($contextIds -contains $_ -or $ruleJson -match [regex]::Escape($_)) }) } $roleDefinitionId = $null if ($ruleJson -match '"roleDefinitionId"\s*:\s*"([0-9a-zA-Z-]+)"') { $roleDefinitionId = $Matches[1] } if (-not $roleDefinitionId -and $policy.PSObject.Properties.Name -contains 'RoleDefinitionId' -and $policy.RoleDefinitionId) { $roleDefinitionId = $policy.RoleDefinitionId } # Map Authentication Context names from both IDs and class references (cN patterns) $contextIdToName = @() if ($AuthContexts) { $allReferences = @(); if ($contextIds) { $allReferences += $contextIds }; if ($contextClasses) { $allReferences += $contextClasses } foreach ($reference in ($allReferences | Sort-Object -Unique)) { $foundContext = $AuthContexts | Where-Object { $_.Id -eq $reference } if ($foundContext) { $contextIdToName += $foundContext.DisplayName } else { $contextIdToName += $reference } } } # Preserve original GroupName if present on raw policy object (needed for PIM for Groups HTML output) $groupNameOriginal = $null if ($policy.PSObject.Properties.Name -contains 'GroupName') { $groupNameOriginal = $policy.GroupName } $output += [pscustomobject]@{ PolicyId = $policy.id ScopeId = $policy.scopeId ScopeType = $policy.scopeType AuthContextIds = ($contextIds | Sort-Object -Unique) -join ',' AuthContextClassRefs = ($contextClasses | Sort-Object -Unique) -join ',' AuthContextNames = ($contextIdToName | Sort-Object -Unique) -join ',' MatchedContextNamesText = ($matchedNames -join ',') RawContainsAuthContext = $true RoleDefinitionId = $roleDefinitionId GroupName = $groupNameOriginal } } if (-not $NoProgress) { Write-Progress -Id 6 -Activity 'PIM Policies' -Completed -Status "Processed $totalPolicies policy object(s)" } return $output } |