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" $results = @{ AzureRolePolicies = @() EntraRolePolicies = @() GroupPolicies = @() Errors = @() Summary = @{ TotalProcessed = 0 Successful = 0 Failed = 0 Skipped = 0 RolesNotFound = 0 } } try { # If the provided Config has no policy sections, nothing to do if (-not ($Config.PSObject.Properties['AzureRolePolicies'] -or $Config.PSObject.Properties['EntraRolePolicies'] -or $Config.PSObject.Properties['GroupPolicies'])) { Write-Verbose "No policy sections present in Config; skipping policy processing." return $results } # Azure Role Policies if ($Config.PSObject.Properties['AzureRolePolicies'] -and $Config.AzureRolePolicies -and $Config.AzureRolePolicies.Count -gt 0) { $whatIfDetails = @() 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 { "" } $policyDetails = @( "Role: '$($policyDef.RoleName)'$protectedWarning", "Scope: '$($policyDef.Scope)'", "Activation Duration: $($resolvedPolicy.ActivationDuration)", "MFA Required: $(if ($resolvedPolicy.ActivationRequirement -match 'MFA') { 'Yes' } else { 'No' })", "Justification Required: $(if ($resolvedPolicy.ActivationRequirement -match 'Justification') { 'Yes' } else { 'No' })", "Approval Required: $($resolvedPolicy.ApprovalRequired)" ) if ($resolvedPolicy.ApprovalRequired -and $resolvedPolicy.PSObject.Properties['Approvers'] -and $resolvedPolicy.Approvers) { $approverList = $resolvedPolicy.Approvers | ForEach-Object { $desc = if ($_.description) { $_.description } else { $_.Name } $idValue = if ($_.id) { $_.id } else { $_.Id } "$desc ($idValue)" } $policyDetails += "Approvers: $($approverList -join ', ')" } if ($resolvedPolicy.PSObject.Properties['AuthenticationContext_Enabled'] -and $resolvedPolicy.AuthenticationContext_Enabled) { $policyDetails += "Authentication Context: $($resolvedPolicy.AuthenticationContext_Value)" } $policyDetails += "Max Eligibility: $($resolvedPolicy.MaximumEligibilityDuration)" $policyDetails += "Permanent Eligibility: $(if ($resolvedPolicy.AllowPermanentEligibility) { 'Allowed' } else { 'Not Allowed' })" $whatIfDetails += " * $($policyDetails -join ' | ')" } $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 "[WARNING] Skipping Azure Role Policies processing due to WhatIf" -ForegroundColor Yellow Write-Host " Would have applied the following policy configurations:" -ForegroundColor Yellow foreach ($line in $whatIfDetails) { Write-Host " $line" -ForegroundColor Yellow } $results.Summary.Skipped += $Config.AzureRolePolicies.Count } } # Entra Role Policies if ($Config.PSObject.Properties['EntraRolePolicies'] -and $Config.EntraRolePolicies -and $Config.EntraRolePolicies.Count -gt 0) { 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 { "" } $roleLabel = if ($policyDef.PSObject.Properties['_RoleNotFound'] -and $policyDef._RoleNotFound) { "Role: '$($policyDef.RoleName)' [NOT FOUND - SKIPPED]" } else { "Role: '$($policyDef.RoleName)'$protectedWarning" } $policyDetails = @( $roleLabel ) if ($policy.PSObject.Properties['ActivationDuration'] -and $policy.ActivationDuration) { $policyDetails += "Activation Duration: $($policy.ActivationDuration)" } else { $policyDetails += "Activation Duration: Not specified" } $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' } } $policyDetails += "Requirements: $(if ($requirements) { $requirements -join ', ' } else { 'None' })" if ($policy.PSObject.Properties['ApprovalRequired'] -and $null -ne $policy.ApprovalRequired) { $policyDetails += "Approval Required: $($policy.ApprovalRequired)" if ($policy.ApprovalRequired) { if ($policy.PSObject.Properties['Approvers'] -and $policy.Approvers) { $approverList = $policy.Approvers | ForEach-Object { if ($_.PSObject.Properties['description'] -and $_.PSObject.Properties['id']) { $desc = if ($_.description) { $_.description } else { $_.Name } $idValue = if ($_.id) { $_.id } else { $_.Id } "$desc ($idValue)" } else { "$_" } } $policyDetails += "Approvers: $($approverList -join ', ')" } else { $policyDetails += "[WARNING: ApprovalRequired is true but no Approvers specified!]" } } } else { $policyDetails += "Approval Required: Not specified" } if ($policy.PSObject.Properties['AuthenticationContext_Enabled'] -and $policy.AuthenticationContext_Enabled -and $policy.PSObject.Properties['AuthenticationContext_Value'] -and $policy.AuthenticationContext_Value) { $policyDetails += "Authentication Context: $($policy.AuthenticationContext_Value)" } if ($policy.PSObject.Properties['MaximumEligibilityDuration'] -and $policy.MaximumEligibilityDuration) { $policyDetails += "Max Eligibility: $($policy.MaximumEligibilityDuration)" } if ($policy.PSObject.Properties['AllowPermanentEligibility'] -and $null -ne $policy.AllowPermanentEligibility) { $policyDetails += "Permanent Eligibility: $(if ($policy.AllowPermanentEligibility) { 'Allowed' } else { 'Not Allowed' })" } $whatIfDetails += " * $($policyDetails -join ' | ')" } $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 "[WARNING] Skipping Entra Role Policies processing due to WhatIf" -ForegroundColor Yellow Write-Host " Would have applied the following policy configurations:" -ForegroundColor Yellow foreach ($line in $whatIfDetails) { Write-Host " $line" -ForegroundColor Yellow } $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.PSObject.Properties['GroupPolicies'] -and $Config.GroupPolicies -and $Config.GroupPolicies.Count -gt 0) { $whatIfDetails = @() foreach ($policyDef in $Config.GroupPolicies) { $resolvedPolicy = if ($policyDef.ResolvedPolicy) { $policyDef.ResolvedPolicy } else { $policyDef } $policyDetails = @( "Group ID: '$($policyDef.GroupId)'", "Role: '$($policyDef.RoleName)'", "Activation Duration: $($resolvedPolicy.ActivationDuration)", "MFA Required: $(if ($resolvedPolicy.ActivationRequirement -match 'MFA') { 'Yes' } else { 'No' })", "Justification Required: $(if ($resolvedPolicy.ActivationRequirement -match 'Justification') { 'Yes' } else { 'No' })", "Approval Required: $($resolvedPolicy.ApprovalRequired)" ) if ($resolvedPolicy.ApprovalRequired -and $resolvedPolicy.PSObject.Properties['Approvers'] -and $resolvedPolicy.Approvers) { $approverList = $resolvedPolicy.Approvers | ForEach-Object { $desc = if ($_.description) { $_.description } else { $_.Name } $idValue = if ($_.id) { $_.id } else { $_.Id } "$desc ($idValue)" } $policyDetails += "Approvers: $($approverList -join ', ')" } if ($resolvedPolicy.PSObject.Properties['AuthenticationContext_Enabled'] -and $resolvedPolicy.AuthenticationContext_Enabled) { $policyDetails += "Authentication Context: $($resolvedPolicy.AuthenticationContext_Value)" } $policyDetails += "Max Eligibility: $($resolvedPolicy.MaximumEligibilityDuration)" $policyDetails += "Permanent Eligibility: $(if ($resolvedPolicy.AllowPermanentEligibility) { 'Allowed' } else { 'Not Allowed' })" $whatIfDetails += " * $($policyDetails -join ' | ')" } $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 "[WARNING] Skipping Group Policies processing due to WhatIf" -ForegroundColor Yellow Write-Host " Would have applied the following policy configurations:" -ForegroundColor Yellow foreach ($line in $whatIfDetails) { Write-Host " $line" -ForegroundColor Yellow } $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 } } |