internal/functions/New-EPOEasyPIMPolicies.ps1
|
#Requires -Version 5.1 function New-EPOEasyPIMPolicies { <# .SYNOPSIS Apply EasyPIM policies across Azure, Entra, and Groups. .DESCRIPTION Processes the provided configuration object, generating and applying policy rules for Azure Resource roles, Entra roles, and Group roles based on PolicyMode (delta/initial). Supports -WhatIf for preview and returns a summarized result object. .PARAMETER Config The PSCustomObject configuration previously loaded (e.g., via Get-EasyPIMConfiguration). .PARAMETER TenantId The target Entra tenant ID. .PARAMETER SubscriptionId The Azure subscription ID for Azure Resource role policies. .PARAMETER PolicyMode One of delta or initial to control application behavior. .EXAMPLE New-EPOEasyPIMPolicies -Config $cfg -TenantId $tid -SubscriptionId $sub -PolicyMode delta -WhatIf Previews configured policy changes without making modifications. .EXAMPLE New-EPOEasyPIMPolicies -Config $cfg -TenantId $tid -SubscriptionId $sub -PolicyMode delta Applies additive changes for roles and groups where needed. .NOTES Returns a summary object with per-domain results and counts. #> [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory=$true)] [PSCustomObject]$Config, [Parameter(Mandatory=$true)] [string]$TenantId, [Parameter(Mandatory=$false)] [string]$SubscriptionId, [Parameter(Mandatory=$false)] [ValidateSet('delta','initial')] [string]$PolicyMode = 'delta', [Parameter(Mandatory=$false)] [switch]$AllowProtectedRoles ) Write-Verbose "Starting New-EPOEasyPIMPolicies in $PolicyMode mode" # Detect WhatIf mode $isWhatIf = $PSCmdlet.ParameterSetName -eq 'WhatIf' -or $PSCmdlet.MyInvocation.BoundParameters.ContainsKey('WhatIf') -or $WhatIfPreference.IsPresent $results = @{ AzureRolePolicies = @() EntraRolePolicies = @() GroupPolicies = @() Errors = @() Summary = @{ TotalProcessed = 0 Successful = 0 Failed = 0 Skipped = 0 RolesNotFound = 0 DriftDetected = 0 } } try { # If the provided Config has no policy sections, nothing to do if (-not ($Config.AzureRolePolicies -or $Config.EntraRolePolicies -or $Config.GroupPolicies)) { Write-Verbose "No policy sections present in Config; skipping policy processing." return $results } # Azure Role Policies if ($Config.AzureRolePolicies -and $Config.AzureRolePolicies.Count -gt 0) { $whatIfDetails = @() # Note: For Azure policies, we do not pre-fetch in bulk because mapping back to RoleName is complex. # We fetch individually in the loop below. foreach ($policyDef in $Config.AzureRolePolicies) { $resolvedPolicy = if ($policyDef.ResolvedPolicy) { $policyDef.ResolvedPolicy } else { $policyDef } # Check if this is a protected Azure role and add warning to WhatIf display $protectedAzureRoles = @("Owner","User Access Administrator") $isProtected = $protectedAzureRoles -contains $policyDef.RoleName $protectedWarning = if ($isProtected) { if (-not $AllowProtectedRoles) { " [⚠️ PROTECTED - BLOCKED]" } else { " [⚠️ PROTECTED - OVERRIDE ENABLED]" } } else { "" } # WhatIf Logic: Check for drift if in WhatIf mode $isDrift = $true $driftReason = "" $live = $null $fetchError = $null if ($isWhatIf) { try { # Determine scope $scope = if ($policyDef.Scope) { $policyDef.Scope } else { "subscriptions/$SubscriptionId" } $subId = $SubscriptionId if ($scope -match 'subscriptions/([^/]+)') { $subId = $matches[1] } # Fetch single policy for comparison $live = Get-PIMAzureResourcePolicy -tenantID $TenantId -rolename $policyDef.RoleName -scope $scope -ErrorAction Stop if ($live -is [array]) { $live = $live | Select-Object -First 1 } if ($live) { $tempResults = @() $tempDrift = 0 # Use Compare-PIMPolicy to check for drift Compare-PIMPolicy -Type 'AzureRole' -Name $policyDef.RoleName -Expected $resolvedPolicy -Live $live -Results ([ref]$tempResults) -DriftCount ([ref]$tempDrift) if ($tempDrift -eq 0) { $isDrift = $false } else { $driftItems = $tempResults | Where-Object { $_.Status -eq 'Drift' } $driftReason = " [DRIFT: $($driftItems.Differences -join ', ')]" } } } catch { $fetchError = $_.Exception.Message Write-Verbose "Could not verify drift for $($policyDef.RoleName): $_" } } if ($isDrift) { if ($isWhatIf) { $results.Summary.DriftDetected++ } $sb = [System.Text.StringBuilder]::new() $null = $sb.AppendLine(" * Role: '$($policyDef.RoleName)'$protectedWarning") $null = $sb.AppendLine(" Scope: '$($policyDef.Scope)'") if ($driftItems) { $null = $sb.AppendLine(" ⚠️ DRIFT:") $null = $sb.AppendLine(" | Setting | Actual | Expected | Drift |") $null = $sb.AppendLine(" |---|---|---|---|") foreach ($item in $driftItems) { # Support both old string-only and new list-based drift items $diffs = if ($item.PSObject.Properties['DifferencesList']) { $item.DifferencesList } else { $item.Differences -split '; ' } foreach ($diff in $diffs) { if ($diff -match "^(?<setting>[^:]+): expected='(?<expected>.*?)' actual='(?<actual>.*?)'(?<note>.*)?$") { $s = $matches['setting'] $e = $matches['expected'] $a = $matches['actual'] $n = $matches['note'] if ($n) { $a += " *" } $null = $sb.AppendLine(" | $s | $a | $e | Yes |") } else { $null = $sb.AppendLine(" | $diff | | | Yes |") } } } } else { if ($isWhatIf -and -not $live) { $msg = " ⚠️ UNKNOWN STATE (Could not fetch live policy)" if ($fetchError) { $msg += ": $fetchError" } $null = $sb.AppendLine($msg) } $null = $sb.AppendLine(" ✅ TARGET STATE:") $null = $sb.AppendLine(" - Activation: $($resolvedPolicy.ActivationDuration)") $reqs = @() if ($resolvedPolicy.ActivationRequirement -match 'MFA') { $reqs += 'MFA' } if ($resolvedPolicy.ActivationRequirement -match 'Justification') { $reqs += 'Justification' } $reqsStr = if ($reqs) { $reqs -join ', ' } else { 'None' } $null = $sb.AppendLine(" - Requirements: $reqsStr") $null = $sb.AppendLine(" - Approval: $($resolvedPolicy.ApprovalRequired)") if ($resolvedPolicy.ApprovalRequired -and $resolvedPolicy.PSObject.Properties['Approvers'] -and $resolvedPolicy.Approvers) { $approverList = $resolvedPolicy.Approvers | ForEach-Object { $item = $_ $desc = $null $idVal = $null if ($item -is [hashtable]) { if ($item.ContainsKey('description')) { $desc = $item['description'] } elseif ($item.ContainsKey('Name')) { $desc = $item['Name'] } if ($item.ContainsKey('id')) { $idVal = $item['id'] } elseif ($item.ContainsKey('Id')) { $idVal = $item['Id'] } } elseif ($item.PSObject) { if ($item.PSObject.Properties['description']) { $desc = $item.description } elseif ($item.PSObject.Properties['Name']) { $desc = $item.Name } if ($item.PSObject.Properties['id']) { $idVal = $item.id } elseif ($item.PSObject.Properties['Id']) { $idVal = $item.Id } } if ($desc -and $idVal) { "$desc ($idVal)" } else { "$item" } } $null = $sb.AppendLine(" - Approvers: $($approverList -join ', ')") } if ($resolvedPolicy.PSObject.Properties['AuthenticationContext_Enabled'] -and $resolvedPolicy.AuthenticationContext_Enabled) { $null = $sb.AppendLine(" - Auth Context: $($resolvedPolicy.AuthenticationContext_Value)") } $null = $sb.AppendLine(" - Max Eligibility: $($resolvedPolicy.MaximumEligibilityDuration)") $permElig = if ($resolvedPolicy.AllowPermanentEligibility) { 'Allowed' } else { 'Not Allowed' } $null = $sb.AppendLine(" - Permanent Eligibility: $permElig") } $whatIfDetails += $sb.ToString() } elseif ($isWhatIf) { $sb = [System.Text.StringBuilder]::new() $null = $sb.AppendLine(" * Role: '$($policyDef.RoleName)'$protectedWarning") $null = $sb.AppendLine(" Scope: '$($policyDef.Scope)'") $null = $sb.AppendLine(" ✅ [MATCH] Configuration matches live state") $whatIfDetails += $sb.ToString() } } if ($whatIfDetails.Count -eq 0 -and $Config.AzureRolePolicies.Count -gt 0) { $whatIfDetails += " * [ALL MATCH] All $($Config.AzureRolePolicies.Count) Azure role policies match the current configuration." } $whatIfMessage = "Apply Azure Role Policy configurations:`n$($whatIfDetails -join "`n")" if ($PSCmdlet.ShouldProcess($whatIfMessage, "Azure Role Policies")) { Write-Host "[PROC] Processing Azure Role Policies..." -ForegroundColor Cyan if (-not $SubscriptionId -and -not ($Config.AzureRolePolicies | Where-Object { $_.Scope })) { $errorMsg = "SubscriptionId is required for Azure Role Policies if no Scope is provided per policy" Write-Error $errorMsg $results.Errors += $errorMsg } else { foreach ($policyDef in $Config.AzureRolePolicies) { $results.Summary.TotalProcessed++ try { $policyResult = Set-EPOAzureRolePolicy -PolicyDefinition $policyDef -TenantId $TenantId -SubscriptionId $SubscriptionId -Mode $PolicyMode -AllowProtectedRoles:$AllowProtectedRoles $results.AzureRolePolicies += $policyResult if ($policyResult.Status -like "*Protected*") { $results.Summary.Skipped++ Write-Host " [PROTECTED] Protected Azure role '$($policyDef.RoleName)' - policy change blocked for security" -ForegroundColor Yellow } elseif ($policyResult.Status -like "Failed*") { $results.Summary.Failed++ Write-Host " [FAIL] Azure role '$($policyDef.RoleName)' at scope '$($policyDef.Scope)' policy apply failed (Status=$($policyResult.Status))" -ForegroundColor Red } elseif ($policyResult.Status -like "Skipped*" -or $policyResult.Status -like "Deferred*") { $results.Summary.Skipped++ Write-Host " [SKIPPED] Azure role '$($policyDef.RoleName)' at scope '$($policyDef.Scope)' (Status=$($policyResult.Status))" -ForegroundColor Yellow } elseif ($policyResult.Status -like "CmdletMissing*") { $results.Summary.Failed++ Write-Host " [FAIL] Azure role '$($policyDef.RoleName)' at scope '$($policyDef.Scope)' required cmdlet missing" -ForegroundColor Red } else { $results.Summary.Successful++ $resolved = $policyDef.ResolvedPolicy if ($resolved) { $act = $resolved.ActivationDuration $reqs = @(); if ($resolved.ActivationRequirement -match 'MFA') { $reqs += 'MFA' }; if ($resolved.ActivationRequirement -match 'Justification') { $reqs += 'Justification' } $reqsTxt = if ($reqs) { $reqs -join '+' } else { 'None' } $appr = if ($resolved.ApprovalRequired) { "Yes($($resolved.Approvers.Count) approvers)" } else { 'No' } $elig = $resolved.MaximumEligibilityDuration $permElig = if ($resolved.AllowPermanentEligibility) { 'Allowed' } else { 'No' } $actMax = $resolved.MaximumActiveAssignmentDuration $permAct = if ($resolved.AllowPermanentActiveAssignment) { 'Allowed' } else { 'No' } $notifCount = ($resolved.PSObject.Properties | Where-Object { $_.Name -like 'Notification_*' }).Count $summary = "Activation=$act Requirements=$reqsTxt Approval=$appr Elig=$elig PermElig=$permElig Active=$actMax PermActive=$permAct Notifications=$notifCount" } else { $summary = '' } Write-Host " [OK] Applied policy for role '$($policyDef.RoleName)' at scope '$($policyDef.Scope)' $summary" -ForegroundColor Green } } catch { $errorMsg = "Failed to apply Azure role policy for '$($policyDef.RoleName)': $($_.Exception.Message)"; Write-Error $errorMsg; $results.Errors += $errorMsg; $results.Summary.Failed++ } } } } else { Write-Host "[INFO] Azure Role Policies application skipped (WhatIf mode)" -ForegroundColor Cyan $results.Summary.Skipped += $Config.AzureRolePolicies.Count } } # Entra Role Policies if ($Config.EntraRolePolicies -and $Config.EntraRolePolicies.Count -gt 0) { # Pre-fetch live policies if in WhatIf mode to filter out matching ones $entraLivePolicies = @{} if ($isWhatIf) { Write-Verbose "WhatIf mode detected: Pre-fetching Entra policies to filter matching configurations..." try { $roleNames = $Config.EntraRolePolicies.RoleName | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } if ($roleNames) { $live = Get-PIMEntraRolePolicy -TenantId $TenantId -RoleName $roleNames -ErrorAction SilentlyContinue if ($live) { foreach ($l in $live) { # Get-PIMEntraRolePolicy returns object with 'roleName' property which matches input if ($l.PSObject.Properties['roleName'] -or ($l -is [hashtable] -and $l.ContainsKey('roleName'))) { $entraLivePolicies[$l.roleName] = $l } } } } } catch { Write-Verbose "Error pre-fetching Entra policies: $_" } } foreach ($policyDef in $Config.EntraRolePolicies) { try { if (-not $policyDef.PSObject.Properties['RoleName'] -or [string]::IsNullOrWhiteSpace($policyDef.RoleName)) { continue } $endpoint = "roleManagement/directory/roleDefinitions?`$filter=displayName eq '$($policyDef.RoleName)'" $resp = invoke-graph -Endpoint $endpoint $found = $false; if ($resp.value -and $resp.value.Count -gt 0) { $found = $true } if (-not $found) { Write-Warning "Entra role '$($policyDef.RoleName)' not found - policy will be skipped. Correct the name to apply this policy."; if (-not $policyDef.PSObject.Properties['_RoleNotFound']) { $policyDef | Add-Member -NotePropertyName _RoleNotFound -NotePropertyValue $true -Force } else { $policyDef._RoleNotFound = $true }; $results.Summary.RolesNotFound++ } else { if (-not $policyDef.PSObject.Properties['_RoleNotFound']) { $policyDef | Add-Member -NotePropertyName _RoleNotFound -NotePropertyValue $false -Force } else { $policyDef._RoleNotFound = $false } } } catch { Write-Warning "Failed to resolve Entra role '$($policyDef.RoleName)': $($_.Exception.Message)" } } $whatIfDetails = @() foreach ($policyDef in $Config.EntraRolePolicies) { $policy = $policyDef.ResolvedPolicy; if (-not $policy) { $policy = $policyDef } # Check if this is a protected role and add warning to WhatIf display $protectedRoles = @("Global Administrator","Privileged Role Administrator","Security Administrator","User Access Administrator") $isProtected = $protectedRoles -contains $policyDef.RoleName $protectedWarning = if ($isProtected) { if (-not $AllowProtectedRoles) { " [⚠️ PROTECTED - BLOCKED]" } else { " [⚠️ PROTECTED - OVERRIDE ENABLED]" } } else { "" } # WhatIf Logic: Check for drift if in WhatIf mode $isDrift = $true $driftReason = "" if ($isWhatIf) { try { $live = $entraLivePolicies[$policyDef.RoleName] if ($live) { $tempResults = @() $tempDrift = 0 Compare-PIMPolicy -Type 'EntraRole' -Name $policyDef.RoleName -Expected $policy -Live $live -Results ([ref]$tempResults) -DriftCount ([ref]$tempDrift) if ($tempDrift -eq 0) { $isDrift = $false } else { $driftItems = $tempResults | Where-Object { $_.Status -eq 'Drift' } $driftReason = " [DRIFT: $($driftItems.Differences -join ', ')]" } } } catch { Write-Verbose "Could not verify drift for $($policyDef.RoleName): $_" } } if ($isDrift) { if ($isWhatIf) { $results.Summary.DriftDetected++ } $sb = [System.Text.StringBuilder]::new() $roleLabel = if ($policyDef.PSObject.Properties['_RoleNotFound'] -and $policyDef._RoleNotFound) { "Role: '$($policyDef.RoleName)' [NOT FOUND - SKIPPED]" } else { "Role: '$($policyDef.RoleName)'$protectedWarning" } $null = $sb.AppendLine(" * $roleLabel") if ($driftItems) { $null = $sb.AppendLine(" ⚠️ DRIFT:") $null = $sb.AppendLine(" | Setting | Actual | Expected | Drift |") $null = $sb.AppendLine(" |---|---|---|---|") foreach ($item in $driftItems) { # Support both old string-only and new list-based drift items $diffs = if ($item.PSObject.Properties['DifferencesList']) { $item.DifferencesList } else { $item.Differences -split '; ' } foreach ($diff in $diffs) { if ($diff -match "^(?<setting>[^:]+): expected='(?<expected>.*?)' actual='(?<actual>.*?)'(?<note>.*)?$") { $s = $matches['setting'] $e = $matches['expected'] $a = $matches['actual'] $n = $matches['note'] if ($n) { $a += " *" } $null = $sb.AppendLine(" | $s | $a | $e | Yes |") } else { $null = $sb.AppendLine(" | $diff | | | Yes |") } } } } else { $null = $sb.AppendLine(" ✅ TARGET STATE:") $actDur = if ($policy.PSObject.Properties['ActivationDuration'] -and $policy.ActivationDuration) { $policy.ActivationDuration } else { "Not specified" } $null = $sb.AppendLine(" - Activation: $actDur") $requirements = @() if ($policy.PSObject.Properties['ActivationRequirement'] -and $policy.ActivationRequirement) { if ($policy.ActivationRequirement -match 'MultiFactorAuthentication' -or $policy.ActivationRequirement -match 'MFA') { $requirements += 'MultiFactorAuthentication' } if ($policy.ActivationRequirement -match 'Justification') { $requirements += 'Justification' } } $reqsStr = if ($requirements) { $requirements -join ', ' } else { 'None' } $null = $sb.AppendLine(" - Requirements: $reqsStr") $apprReq = if ($policy.PSObject.Properties['ApprovalRequired'] -and $null -ne $policy.ApprovalRequired) { $policy.ApprovalRequired } else { "Not specified" } $null = $sb.AppendLine(" - Approval: $apprReq") if ($policy.ApprovalRequired) { if ($policy.PSObject.Properties['Approvers'] -and $policy.Approvers) { $approverList = $policy.Approvers | ForEach-Object { $item = $_ $desc = $null $idVal = $null if ($item -is [hashtable]) { if ($item.ContainsKey('description')) { $desc = $item['description'] } elseif ($item.ContainsKey('Name')) { $desc = $item['Name'] } if ($item.ContainsKey('id')) { $idVal = $item['id'] } elseif ($item.ContainsKey('Id')) { $idVal = $item['Id'] } } elseif ($item.PSObject) { if ($item.PSObject.Properties['description']) { $desc = $item.description } elseif ($item.PSObject.Properties['Name']) { $desc = $item.Name } if ($item.PSObject.Properties['id']) { $idVal = $item.id } elseif ($item.PSObject.Properties['Id']) { $idVal = $item.Id } } if ($desc -and $idVal) { "$desc ($idVal)" } else { "$item" } } $null = $sb.AppendLine(" - Approvers: $($approverList -join ', ')") } else { $null = $sb.AppendLine(" - Approvers: [WARNING: ApprovalRequired is true but no Approvers specified!]") } } if ($policy.PSObject.Properties['AuthenticationContext_Enabled'] -and $policy.AuthenticationContext_Enabled -and $policy.PSObject.Properties['AuthenticationContext_Value'] -and $policy.AuthenticationContext_Value) { $null = $sb.AppendLine(" - Auth Context: $($policy.AuthenticationContext_Value)") } if ($policy.PSObject.Properties['MaximumEligibilityDuration'] -and $policy.MaximumEligibilityDuration) { $null = $sb.AppendLine(" - Max Eligibility: $($policy.MaximumEligibilityDuration)") } if ($policy.PSObject.Properties['AllowPermanentEligibility'] -and $null -ne $policy.AllowPermanentEligibility) { $permElig = if ($policy.AllowPermanentEligibility) { 'Allowed' } else { 'Not Allowed' } $null = $sb.AppendLine(" - Permanent Eligibility: $permElig") } } $whatIfDetails += $sb.ToString() } elseif ($isWhatIf) { $sb = [System.Text.StringBuilder]::new() $roleLabel = if ($policyDef.PSObject.Properties['_RoleNotFound'] -and $policyDef._RoleNotFound) { "Role: '$($policyDef.RoleName)' [NOT FOUND - SKIPPED]" } else { "Role: '$($policyDef.RoleName)'$protectedWarning" } $null = $sb.AppendLine(" * $roleLabel") $null = $sb.AppendLine(" ✅ [MATCH] Configuration matches live state") $whatIfDetails += $sb.ToString() } } if ($whatIfDetails.Count -eq 0 -and $Config.EntraRolePolicies.Count -gt 0) { $whatIfDetails += " * [ALL MATCH] All $($Config.EntraRolePolicies.Count) Entra role policies match the current configuration." } $whatIfMessage = "Apply Entra Role Policy configurations:`n$($whatIfDetails -join "`n")" if ($PSCmdlet.ShouldProcess($whatIfMessage, "Entra Role Policies")) { Write-Host "[PROC] Processing Entra Role Policies..." -ForegroundColor Cyan foreach ($policyDef in $Config.EntraRolePolicies) { if ($policyDef.PSObject.Properties['_RoleNotFound'] -and $policyDef._RoleNotFound) { $results.EntraRolePolicies += [PSCustomObject]@{ RoleName = $policyDef.RoleName; Status = 'SkippedRoleNotFound'; Mode = $PolicyMode; Details = 'Role displayName not found during pre-validation' }; $results.Summary.Skipped++; continue } $results.Summary.TotalProcessed++ try { $policyResult = Set-EPOEntraRolePolicy -PolicyDefinition $policyDef -TenantId $TenantId -Mode $PolicyMode -AllowProtectedRoles:$AllowProtectedRoles $results.EntraRolePolicies += $policyResult if ($policyResult.Status -like "*Protected*") { $results.Summary.Skipped++ Write-Host " [PROTECTED] Protected role '$($policyDef.RoleName)' - policy change blocked for security" -ForegroundColor Yellow } elseif ($policyResult.Status -like "Failed*") { $results.Summary.Failed++ Write-Host " [FAIL] Entra role '$($policyDef.RoleName)' policy apply failed (Status=$($policyResult.Status))" -ForegroundColor Red } elseif ($policyResult.Status -like "Skipped*" -or $policyResult.Status -like "Deferred*") { $results.Summary.Skipped++ Write-Host " [SKIPPED] Entra role '$($policyDef.RoleName)' (Status=$($policyResult.Status))" -ForegroundColor Yellow } elseif ($policyResult.Status -like "CmdletMissing*") { $results.Summary.Failed++ Write-Host " [FAIL] Entra role '$($policyDef.RoleName)' required cmdlet missing" -ForegroundColor Red } else { $results.Summary.Successful++ $resolved = $policyDef.ResolvedPolicy if ($resolved) { $act = $resolved.ActivationDuration $reqs = @(); if ($resolved.ActivationRequirement -match 'MFA') { $reqs += 'MFA' }; if ($resolved.ActivationRequirement -match 'Justification') { $reqs += 'Justification' } $reqsTxt = if ($reqs) { $reqs -join '+' } else { 'None' } $appr = if ($resolved.ApprovalRequired) { "Yes($($resolved.Approvers.Count) approvers)" } else { 'No' } $elig = $resolved.MaximumEligibilityDuration $permElig = if ($resolved.AllowPermanentEligibility) { 'Allowed' } else { 'No' } $actMax = $resolved.MaximumActiveAssignmentDuration $permAct = if ($resolved.AllowPermanentActiveAssignment) { 'Allowed' } else { 'No' } $notifCount = ($resolved.PSObject.Properties | Where-Object { $_.Name -like 'Notification_*' }).Count $summary = "Activation=$act Requirements=$reqsTxt Approval=$appr Elig=$elig PermElig=$permElig Active=$actMax PermActive=$permAct Notifications=$notifCount" } else { $summary = '' } Write-Host " [OK] Applied policy for Entra role '$($policyDef.RoleName)' $summary" -ForegroundColor Green } } catch { $errorMsg = "Failed to apply Entra role policy for '$($policyDef.RoleName)': $($_.Exception.Message)"; Write-Error $errorMsg; $results.Errors += $errorMsg; $results.Summary.Failed++ } } } else { Write-Host "[INFO] Entra Role Policies application skipped (WhatIf mode)" -ForegroundColor Cyan $results.Summary.Skipped += $Config.EntraRolePolicies.Count foreach ($policyDef in $Config.EntraRolePolicies | Where-Object { $_.PSObject.Properties['_RoleNotFound'] -and $_._RoleNotFound }) { $results.EntraRolePolicies += [PSCustomObject]@{ RoleName = $policyDef.RoleName; Status = 'SkippedRoleNotFound'; Mode = $PolicyMode; Details = 'Role displayName not found during pre-validation' } } } } # Group Policies if ($Config.GroupPolicies -and $Config.GroupPolicies.Count -gt 0) { # Pre-fetch live policies if in WhatIf mode to filter out matching ones $groupLivePolicies = @{} if ($isWhatIf) { Write-Verbose "WhatIf mode detected: Pre-fetching Group policies to filter matching configurations..." try { # Group by GroupId to optimize calls $policiesByGroup = $Config.GroupPolicies | Group-Object -Property GroupId foreach ($groupGroup in $policiesByGroup) { $groupId = $groupGroup.Name $roles = $groupGroup.Group.RoleName if ($groupId -and $roles) { try { # Get-PIMGroupPolicy requires GroupID and Type (owner/member). # It does not support filtering by an array of RoleNames directly. # Therefore, we iterate through each unique role type (owner/member) present in the configuration # to fetch the corresponding policies. $types = $roles | Select-Object -Unique foreach ($type in $types) { $live = Get-PIMGroupPolicy -tenantID $TenantId -groupID $groupId -type $type -ErrorAction SilentlyContinue if ($live -is [array]) { $live = $live | Select-Object -First 1 } if ($live) { # Key needs to be unique per group+role $key = "$groupId|$type" $groupLivePolicies[$key] = $live } } } catch { Write-Verbose "Failed to fetch live Group policy for group $($groupId): $_" } } } } catch { Write-Verbose "Error pre-fetching Group policies: $_" } } $whatIfDetails = @() foreach ($policyDef in $Config.GroupPolicies) { $resolvedPolicy = if ($policyDef.ResolvedPolicy) { $policyDef.ResolvedPolicy } else { $policyDef } # WhatIf Logic: Check for drift if in WhatIf mode $isDrift = $true $driftReason = "" if ($isWhatIf) { try { $key = "$($policyDef.GroupId)|$($policyDef.RoleName)" $live = $groupLivePolicies[$key] if ($live) { $tempResults = @() $tempDrift = 0 Compare-PIMPolicy -Type 'Group' -Name $policyDef.RoleName -Expected $resolvedPolicy -Live $live -ExtraId $policyDef.GroupId -Results ([ref]$tempResults) -DriftCount ([ref]$tempDrift) if ($tempDrift -eq 0) { $isDrift = $false } else { $driftItems = $tempResults | Where-Object { $_.Status -eq 'Drift' } $driftReason = " [DRIFT: $($driftItems.Differences -join ', ')]" } } } catch { Write-Verbose "Could not verify drift for Group $($policyDef.GroupId) role $($policyDef.RoleName): $_" } } if ($isDrift) { if ($isWhatIf) { $results.Summary.DriftDetected++ } $sb = [System.Text.StringBuilder]::new() $null = $sb.AppendLine(" * Group ID: '$($policyDef.GroupId)'") $null = $sb.AppendLine(" Role: '$($policyDef.RoleName)'") if ($driftItems) { $null = $sb.AppendLine(" ⚠️ DRIFT:") $null = $sb.AppendLine(" | Setting | Actual | Expected | Drift |") $null = $sb.AppendLine(" |---|---|---|---|") foreach ($item in $driftItems) { # Support both old string-only and new list-based drift items $diffs = if ($item.PSObject.Properties['DifferencesList']) { $item.DifferencesList } else { $item.Differences -split '; ' } foreach ($diff in $diffs) { if ($diff -match "^(?<setting>[^:]+): expected='(?<expected>.*?)' actual='(?<actual>.*?)'(?<note>.*)?$") { $s = $matches['setting'] $e = $matches['expected'] $a = $matches['actual'] $n = $matches['note'] if ($n) { $a += " *" } $null = $sb.AppendLine(" | $s | $a | $e | Yes |") } else { $null = $sb.AppendLine(" | $diff | | | Yes |") } } } } else { $null = $sb.AppendLine(" ✅ TARGET STATE:") $actDur = if ($resolvedPolicy.PSObject.Properties['ActivationDuration'] -and $resolvedPolicy.ActivationDuration) { $resolvedPolicy.ActivationDuration } else { "Not specified" } $null = $sb.AppendLine(" - Activation: $actDur") $requirements = @() if ($resolvedPolicy.PSObject.Properties['ActivationRequirement'] -and $resolvedPolicy.ActivationRequirement) { if ($resolvedPolicy.ActivationRequirement -match 'MFA') { $requirements += 'MFA' } if ($resolvedPolicy.ActivationRequirement -match 'Justification') { $requirements += 'Justification' } } $reqsStr = if ($requirements) { $requirements -join ', ' } else { 'None' } $null = $sb.AppendLine(" - Requirements: $reqsStr") $apprReq = if ($resolvedPolicy.PSObject.Properties['ApprovalRequired'] -and $null -ne $resolvedPolicy.ApprovalRequired) { $resolvedPolicy.ApprovalRequired } else { "Not specified" } $null = $sb.AppendLine(" - Approval: $apprReq") if ($resolvedPolicy.ApprovalRequired) { if ($resolvedPolicy.PSObject.Properties['Approvers'] -and $resolvedPolicy.Approvers) { $approverList = $resolvedPolicy.Approvers | ForEach-Object { $item = $_ $desc = $null $idVal = $null if ($item -is [hashtable]) { if ($item.ContainsKey('description')) { $desc = $item['description'] } elseif ($item.ContainsKey('Name')) { $desc = $item['Name'] } if ($item.ContainsKey('id')) { $idVal = $item['id'] } elseif ($item.ContainsKey('Id')) { $idVal = $item['Id'] } } elseif ($item.PSObject) { if ($item.PSObject.Properties['description']) { $desc = $item.description } elseif ($item.PSObject.Properties['Name']) { $desc = $item.Name } if ($item.PSObject.Properties['id']) { $idVal = $item.id } elseif ($item.PSObject.Properties['Id']) { $idVal = $item.Id } } if ($desc -and $idVal) { "$desc ($idVal)" } else { "$item" } } $null = $sb.AppendLine(" - Approvers: $($approverList -join ', ')") } else { $null = $sb.AppendLine(" - Approvers: [WARNING: ApprovalRequired is true but no Approvers specified!]") } } if ($resolvedPolicy.PSObject.Properties['AuthenticationContext_Enabled'] -and $resolvedPolicy.AuthenticationContext_Enabled) { $null = $sb.AppendLine(" - Auth Context: $($resolvedPolicy.AuthenticationContext_Value)") } if ($resolvedPolicy.PSObject.Properties['MaximumEligibilityDuration'] -and $resolvedPolicy.MaximumEligibilityDuration) { $null = $sb.AppendLine(" - Max Eligibility: $($resolvedPolicy.MaximumEligibilityDuration)") } if ($resolvedPolicy.PSObject.Properties['AllowPermanentEligibility'] -and $null -ne $resolvedPolicy.AllowPermanentEligibility) { $permElig = if ($resolvedPolicy.AllowPermanentEligibility) { 'Allowed' } else { 'Not Allowed' } $null = $sb.AppendLine(" - Permanent Eligibility: $permElig") } } $whatIfDetails += $sb.ToString() } elseif ($isWhatIf) { $sb = [System.Text.StringBuilder]::new() $null = $sb.AppendLine(" * Group ID: '$($policyDef.GroupId)'") $null = $sb.AppendLine(" Role: '$($policyDef.RoleName)'") $null = $sb.AppendLine(" ✅ [MATCH] Configuration matches live state") $whatIfDetails += $sb.ToString() } } if ($whatIfDetails.Count -eq 0 -and $Config.GroupPolicies.Count -gt 0) { $whatIfDetails += " * [ALL MATCH] All $($Config.GroupPolicies.Count) Group policies match the current configuration." } $whatIfMessage = "Apply Group Policy configurations:`n$($whatIfDetails -join "`n")" if ($PSCmdlet.ShouldProcess($whatIfMessage, "Group Policies")) { Write-Host "[PROC] Processing Group Policies..." -ForegroundColor Cyan foreach ($policyDef in $Config.GroupPolicies) { $results.Summary.TotalProcessed++ try { $policyResult = Set-EPOGroupPolicy -PolicyDefinition $policyDef -TenantId $TenantId -Mode $PolicyMode $results.GroupPolicies += $policyResult if ($policyResult.Status -like "*Protected*") { $results.Summary.Skipped++ Write-Host " [PROTECTED] Protected Group '$($policyDef.GroupId)' role '$($policyDef.RoleName)' - policy change blocked for security" -ForegroundColor Yellow } elseif ($policyResult.Status -like "Failed*") { $results.Summary.Failed++ Write-Host " [FAIL] Group '$($policyDef.GroupId)' role '$($policyDef.RoleName)' policy apply failed (Status=$($policyResult.Status))" -ForegroundColor Red } elseif ($policyResult.Status -like "Skipped*" -or $policyResult.Status -like "Deferred*") { $results.Summary.Skipped++ Write-Host " [SKIPPED] Group '$($policyDef.GroupId)' role '$($policyDef.RoleName)' (Status=$($policyResult.Status))" -ForegroundColor Yellow } elseif ($policyResult.Status -like "CmdletMissing*") { $results.Summary.Failed++ Write-Host " [FAIL] Group '$($policyDef.GroupId)' role '$($policyDef.RoleName)' required cmdlet missing" -ForegroundColor Red } else { $results.Summary.Successful++ $resolved = $policyDef.ResolvedPolicy if ($resolved) { $act = $resolved.ActivationDuration $reqs = @(); if ($resolved.ActivationRequirement -match 'MFA') { $reqs += 'MFA' }; if ($resolved.ActivationRequirement -match 'Justification') { $reqs += 'Justification' } $reqsTxt = if ($reqs) { $reqs -join '+' } else { 'None' } $appr = if ($resolved.ApprovalRequired) { "Yes($($resolved.Approvers.Count) approvers)" } else { 'No' } $elig = $resolved.MaximumEligibilityDuration $permElig = if ($resolved.AllowPermanentEligibility) { 'Allowed' } else { 'No' } $actMax = $resolved.MaximumActiveAssignmentDuration $permAct = if ($resolved.AllowPermanentActiveAssignment) { 'Allowed' } else { 'No' } $notifCount = ($resolved.PSObject.Properties | Where-Object { $_.Name -like 'Notification_*' }).Count $summary = "Activation=$act Requirements=$reqsTxt Approval=$appr Elig=$elig PermElig=$permElig Active=$actMax PermActive=$permAct Notifications=$notifCount" } else { $summary = '' } Write-Host " [OK] Applied policy for Group '$($policyDef.GroupId)' role '$($policyDef.RoleName)' $summary" -ForegroundColor Green } } catch { $errorMsg = "Failed to apply Group policy for '$($policyDef.GroupId)' role '$($policyDef.RoleName)': $($_.Exception.Message)"; Write-Error $errorMsg; $results.Errors += $errorMsg; $results.Summary.Failed++ } } } else { Write-Host "[INFO] Group Policies application skipped (WhatIf mode)" -ForegroundColor Cyan $results.Summary.Skipped += $Config.GroupPolicies.Count } } Write-Verbose "New-EPOEasyPIMPolicies completed. Processed: $($results.Summary.TotalProcessed), Successful: $($results.Summary.Successful), Failed: $($results.Summary.Failed)" return $results } catch { Write-Error "Failed to process PIM policies: $($_.Exception.Message)"; throw } } |