EasyPIM.psm1
function Get-EasyPIMJustification { [CmdletBinding()] [OutputType([string])] param ( [Parameter()] [string]$CustomJustification, [Parameter()] [switch]$IncludeTimestamp ) if (-not [string]::IsNullOrWhiteSpace($CustomJustification)) { return $CustomJustification } $justification = "Created by EasyPIM Orchestrator" if ($IncludeTimestamp) { $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $justification += " at $timestamp" } return $justification } function Expand-AssignmentWithPrincipalIds { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param ( [array]$Assignments ) $expandedAssignments = @() foreach ($assignment in $Assignments) { # If using PrincipalIds array, expand to individual assignments if ($assignment.PrincipalIds) { foreach ($principalId in $assignment.PrincipalIds) { $newAssignment = $assignment.PSObject.Copy() $newAssignment.PSObject.Properties.Remove('PrincipalIds') $newAssignment | Add-Member -MemberType NoteProperty -Name "PrincipalId" -Value $principalId $expandedAssignments += $newAssignment } } # If using regular PrincipalId, add as-is elseif ($assignment.PrincipalId) { $expandedAssignments += $assignment } # No principal defined - log error else { Write-Warning "Assignment missing both PrincipalId and PrincipalIds properties: $($assignment | ConvertTo-Json -Compress)" } } return $expandedAssignments } function Get-EasyPIMConfiguration { [CmdletBinding(DefaultParameterSetName = 'FilePath')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, ParameterSetName = 'KeyVault')] [string]$KeyVaultName, [Parameter(Mandatory = $true, ParameterSetName = 'KeyVault')] [string]$SecretName, [Parameter(Mandatory = $true, ParameterSetName = 'FilePath')] [string]$ConfigFilePath ) Write-SectionHeader "Retrieving Configuration" # Load configuration from appropriate source if ($PSCmdlet.ParameterSetName -eq 'KeyVault') { Write-Host "Reading from Key Vault '$KeyVaultName', Secret '$SecretName'" -ForegroundColor Cyan $secret = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name $SecretName $jsonContent = $secret.SecretValue | ConvertFrom-SecureString -AsPlainText | Remove-JsonComments } else { Write-Host "Reading from file '$ConfigFilePath'" -ForegroundColor Cyan $jsonContent = Get-Content -Path $ConfigFilePath -Raw | Remove-JsonComments } # Parse and return the configuration return $jsonContent | ConvertFrom-Json } function Initialize-EasyPIMAssignments { [CmdletBinding()] [OutputType([PSCustomObject])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param ( [Parameter(Mandatory = $true)] [PSCustomObject]$Config ) # Generate standard justification $justification = Get-EasyPIMJustification -IncludeTimestamp # Initialize the processed config $processedConfig = [PSCustomObject]@{ AzureRoles = @() AzureRolesActive = @() EntraIDRoles = @() EntraIDRolesActive = @() GroupRoles = @() GroupRolesActive = @() ProtectedUsers = @() Justification = $justification # Store for reference } # Expand all assignments with PrincipalIds arrays if ($Config.AzureRoles) { $processedConfig.AzureRoles = Expand-AssignmentWithPrincipalIds -Assignments $Config.AzureRoles Write-Verbose "Expanded $($Config.AzureRoles.Count) Azure role configs into $($processedConfig.AzureRoles.Count) individual assignments" } if ($Config.AzureRolesActive) { $processedConfig.AzureRolesActive = Expand-AssignmentWithPrincipalIds -Assignments $Config.AzureRolesActive # Ensure RoleName is consistent (some use Role instead) $processedConfig.AzureRolesActive = $processedConfig.AzureRolesActive | ForEach-Object { if (!$_.Rolename -and $_.Role) { $_ | Add-Member -NotePropertyName "Rolename" -NotePropertyValue $_.Role -Force -PassThru } else { $_ } } } if ($Config.EntraIDRoles) { $processedConfig.EntraIDRoles = Expand-AssignmentWithPrincipalIds -Assignments $Config.EntraIDRoles } if ($Config.EntraIDRolesActive) { $processedConfig.EntraIDRolesActive = Expand-AssignmentWithPrincipalIds -Assignments $Config.EntraIDRolesActive } if ($Config.GroupRoles) { $processedConfig.GroupRoles = Expand-AssignmentWithPrincipalIds -Assignments $Config.GroupRoles } if ($Config.GroupRolesActive) { $processedConfig.GroupRolesActive = Expand-AssignmentWithPrincipalIds -Assignments $Config.GroupRolesActive } # Copy protected users if ($Config.ProtectedUsers) { $processedConfig.ProtectedUsers = $Config.ProtectedUsers } return $processedConfig } function Invoke-EasyPIMCleanup { [CmdletBinding(SupportsShouldProcess=$true)] [OutputType([PSCustomObject])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] param ( [Parameter(Mandatory = $true)] [PSCustomObject]$Config, [Parameter(Mandatory = $true)] [ValidateSet('initial', 'delta')] [string]$Mode, [Parameter(Mandatory = $true)] [string]$TenantId, [Parameter()] [string]$SubscriptionId ) Write-SectionHeader "Processing Cleanup" $results = [PSCustomObject]@{ Kept = 0 Removed = 0 Skipped = 0 Protected = 0 } if ($Mode -eq 'initial') { # Call the existing initial cleanup function $initialResult = Invoke-InitialCleanup -Config $Config ` -TenantId $TenantId ` -SubscriptionId $SubscriptionId ` -AzureRoles $Config.AzureRoles ` -AzureRolesActive $Config.AzureRolesActive ` -EntraRoles $Config.EntraIDRoles ` -EntraRolesActive $Config.EntraIDRolesActive ` -GroupRoles $Config.GroupRoles ` -GroupRolesActive $Config.GroupRolesActive $results.Kept = $initialResult.KeptCount $results.Removed = $initialResult.RemovedCount $results.Skipped = $initialResult.SkippedCount $results.Protected = $initialResult.ProtectedCount } else { # Delta mode cleanup Write-Host "=== Performing Delta Mode Cleanup ===" -ForegroundColor Yellow # Azure Role eligible delta cleanup if ($Config.AzureRoles) { Write-SubHeader "Azure Role Eligible Assignments Cleanup" $subscriptions = @($Config.AzureRoles.Scope | ForEach-Object { $_.Split("/")[2] } | Select-Object -Unique) $apiInfo = @{ Subscriptions = $subscriptions TenantId = $TenantId RemoveCmd = "Remove-PIMAzureResourceEligibleAssignment" } $keptCounter = 0 $removeCounter = 0 $skipCounter = 0 Invoke-DeltaCleanup -ResourceType "Azure Role eligible" -ConfigAssignments $Config.AzureRoles -ApiInfo $apiInfo -ProtectedUsers $Config.ProtectedUsers -KeptCounter ([ref]$keptCounter) -RemoveCounter ([ref]$removeCounter) -SkipCounter ([ref]$skipCounter) $results.Kept += $keptCounter $results.Removed += $removeCounter $results.Skipped += $skipCounter } # Azure Role active delta cleanup if ($Config.AzureRolesActive) { Write-SubHeader "Azure Role Active Assignments Cleanup" $subscriptions = @($Config.AzureRolesActive.Scope | ForEach-Object { $_.Split("/")[2] } | Select-Object -Unique) $apiInfo = @{ Subscriptions = $subscriptions ApiEndpoint = "https://management.azure.com/subscriptions/$($subscriptions[0])/providers/Microsoft.Authorization/roleAssignmentScheduleRequests" TargetIdProperty = "targetRoleAssignmentScheduleId" RemoveCmd = "Remove-PIMAzureResourceActiveAssignment" TenantId = $TenantId } $keptCounter = 0 $removeCounter = 0 $skipCounter = 0 Invoke-DeltaCleanup -ResourceType "Azure Role active" -ConfigAssignments $Config.AzureRolesActive -ApiInfo $apiInfo -ProtectedUsers $Config.ProtectedUsers -KeptCounter ([ref]$keptCounter) -RemoveCounter ([ref]$removeCounter) -SkipCounter ([ref]$skipCounter) $results.Kept += $keptCounter $results.Removed += $removeCounter $results.Skipped += $skipCounter } # Entra Role eligible delta cleanup if ($Config.EntraIDRoles) { Write-SubHeader "Entra Role Eligible Assignments Cleanup" $apiInfo = @{ Subscriptions = @() # Not needed for Entra roles RemoveCmd = "Remove-PIMEntraRoleEligibleAssignment" TenantId = $TenantId } $keptCounter = 0 $removeCounter = 0 $skipCounter = 0 Invoke-DeltaCleanup -ResourceType "Entra Role eligible" -ConfigAssignments $Config.EntraIDRoles -ApiInfo $apiInfo -ProtectedUsers $Config.ProtectedUsers -KeptCounter ([ref]$keptCounter) -RemoveCounter ([ref]$removeCounter) -SkipCounter ([ref]$skipCounter) $results.Kept += $keptCounter $results.Removed += $removeCounter $results.Skipped += $skipCounter } # Entra Role active delta cleanup if ($Config.EntraIDRolesActive) { Write-SubHeader "Entra Role Active Assignments Cleanup" $apiInfo = @{ Subscriptions = @() # Not needed for Entra roles RemoveCmd = "Remove-PIMEntraRoleActiveAssignment" TenantId = $TenantId } $keptCounter = 0 $removeCounter = 0 $skipCounter = 0 Invoke-DeltaCleanup -ResourceType "Entra Role active" -ConfigAssignments $Config.EntraIDRolesActive -ApiInfo $apiInfo -ProtectedUsers $Config.ProtectedUsers -KeptCounter ([ref]$keptCounter) -RemoveCounter ([ref]$removeCounter) -SkipCounter ([ref]$skipCounter) $results.Kept += $keptCounter $results.Removed += $removeCounter $results.Skipped += $skipCounter } # Group Role eligible cleanup if ($Config.GroupRoles -and $Config.GroupRoles.Count -gt 0) { Write-SubHeader "Group Role Eligible Assignments Cleanup" # Group by GroupId for processing $groupsByGroupId = $Config.GroupRoles | Group-Object -Property GroupId foreach ($group in $groupsByGroupId) { $groupId = $group.Name $groupAssignments = $group.Group # Validate group exists and is eligible for PIM try { $uri = "https://graph.microsoft.com/v1.0/directoryObjects/$groupId" Invoke-MgGraphRequest -Uri $uri -Method GET -ErrorAction Stop Write-Verbose "Group $groupId exists and is accessible" # Check if group is eligible for PIM if (-not (Test-GroupEligibleForPIM -GroupId $groupId)) { Write-Warning "⚠️ Group $groupId is not eligible for PIM management (likely synced from on-premises), skipping cleanup" $results.Skipped += $groupAssignments.Count continue # Skip this group entirely } } catch { Write-Warning "⚠️ Group $groupId does not exist, skipping cleanup for this group" $results.Skipped += $groupAssignments.Count continue } Write-Host "Processing group: $groupId with $($groupAssignments.Count) desired assignments" # Create API info with this specific group ID $apiInfo = @{ TenantId = $TenantId GroupIds = @($groupId) RemoveCmd = "Remove-PIMGroupEligibleAssignment" } # Call delta cleanup for this group $keptCounter = 0 $removeCounter = 0 $skipCounter = 0 Invoke-DeltaCleanup -ResourceType "Group eligible" -ConfigAssignments $groupAssignments -ApiInfo $apiInfo -ProtectedUsers $Config.ProtectedUsers -KeptCounter ([ref]$keptCounter) -RemoveCounter ([ref]$removeCounter) -SkipCounter ([ref]$skipCounter) # Update results $results.Kept += $keptCounter $results.Removed += $removeCounter $results.Skipped += $skipCounter } } # Group Role active cleanup if ($Config.GroupRolesActive -and $Config.GroupRolesActive.Count -gt 0) { Write-SubHeader "Group Role Active Assignments Cleanup" # Group by GroupId for processing $groupsByGroupId = $Config.GroupRolesActive | Group-Object -Property GroupId foreach ($group in $groupsByGroupId) { $groupId = $group.Name $groupAssignments = $group.Group # Validate group exists and is eligible for PIM try { $uri = "https://graph.microsoft.com/v1.0/directoryObjects/$groupId" Invoke-MgGraphRequest -Uri $uri -Method GET -ErrorAction Stop Write-Verbose "Group $groupId exists and is accessible" # Check if group is eligible for PIM if (-not (Test-GroupEligibleForPIM -GroupId $groupId)) { Write-Warning "⚠️ Group $groupId is not eligible for PIM management (likely synced from on-premises), skipping cleanup" $results.Skipped += $groupAssignments.Count continue # Skip this group entirely } } catch { Write-Warning "⚠️ Group $groupId does not exist, skipping cleanup for this group" $results.Skipped += $groupAssignments.Count continue } Write-Host "Processing group: $groupId with $($groupAssignments.Count) desired assignments" # Create API info with this specific group ID $apiInfo = @{ TenantId = $TenantId GroupIds = @($groupId) RemoveCmd = "Remove-PIMGroupActiveAssignment" } # Call delta cleanup for this group $keptCounter = 0 $removeCounter = 0 $skipCounter = 0 Invoke-DeltaCleanup -ResourceType "Group active" -ConfigAssignments $groupAssignments -ApiInfo $apiInfo -ProtectedUsers $Config.ProtectedUsers -KeptCounter ([ref]$keptCounter) -RemoveCounter ([ref]$removeCounter) -SkipCounter ([ref]$skipCounter) # Update results $results.Kept += $keptCounter $results.Removed += $removeCounter $results.Skipped += $skipCounter } } } Write-Verbose "Cleanup completed. Kept: $($results.Kept), Removed: $($results.Removed), Skipped: $($results.Skipped)" return $results } # Define script-level counters at the top of the file (outside any function) $script:keptCounter = 0 $script:removeCounter = 0 $script:skipCounter = 0 # Define protected roles at script level $script:protectedRoles = @( "User Access Administrator", "Global Administrator", "Privileged Role Administrator", "Security Administrator" ) function Invoke-DeltaCleanup { [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([System.Collections.Hashtable])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] param ( [string]$ResourceType, [array]$ConfigAssignments, [hashtable]$ApiInfo, [array]$ProtectedUsers, # Keep these parameters for compatibility - they're optional now [Parameter(Mandatory = $false)] [ref]$KeptCounter, [Parameter(Mandatory = $false)] [ref]$RemoveCounter, [Parameter(Mandatory = $false)] [ref]$SkipCounter ) # Reset script counters at beginning of function call $script:keptCounter = 0 $script:removeCounter = 0 $script:skipCounter = 0 # At the end of the function (around line 560), add a tracking variable for protected users: $protectedCounter = 0 #region Prevent duplicate calls # Simple solution: track using a hashtable of processed resource types if (-not $script:ProcessedCleanups) { $script:ProcessedCleanups = @{} } $uniqueKey = $ResourceType if ($ApiInfo.GroupId) { $uniqueKey += "-$($ApiInfo.GroupId)" } if ($script:ProcessedCleanups.ContainsKey($uniqueKey)) { Write-host "`n⚠️ Cleanup for '$ResourceType' already processed - skipping duplicate call`n" return } # Mark as processed $script:ProcessedCleanups[$uniqueKey] = (Get-Date) #endregion #region Setup # Display header section Write-Host "`n┌────────────────────────────────────────────────────┐" -ForegroundColor Cyan Write-Host "│ Processing $ResourceType Delta Cleanup" -ForegroundColor Cyan Write-Host "└────────────────────────────────────────────────────┘`n" -ForegroundColor Cyan if ($config.SubscriptionBased) { foreach ($subscription in $ApiInfo.Subscriptions) { Write-Host " 🔍 Checking subscription: $subscription" -ForegroundColor White # Get the assignments if ($subscription -and $config.SubscriptionBased) { Write-Verbose "Getting assignments for subscription: $subscription" $allAssignments = Get-PIMAzureResourceEligibleAssignment -SubscriptionId $subscription -TenantId $ApiInfo.TenantID Write-Host " ├─ Found $($allAssignments.Count) total current assignments" -ForegroundColor White } } } # Create a tracking set for processed assignments to avoid duplicates $processedAssignments = @{} # Define resource type specific settings directly $config = $null switch ($ResourceType) { "Azure Role eligible" { $config = @{ ApiEndpoint = "/providers/Microsoft.Authorization/roleEligibilityScheduleRequests" ApiVersion = "2020-10-01" RemoveCmd = "Remove-PIMAzureResourceEligibleAssignment" SubscriptionBased = $true Filter = "status eq 'Provisioned'" } } "Azure Role active" { $config = @{ ApiEndpoint = "/providers/Microsoft.Authorization/roleAssignmentScheduleRequests" ApiVersion = "2020-10-01" RemoveCmd = "Remove-PIMAzureResourceActiveAssignment" SubscriptionBased = $true Filter = "status eq 'Provisioned'" } } "Entra Role eligible" { $config = @{ ApiEndpoint = "https://graph.microsoft.com/beta/roleManagement/directory/roleEligibilityScheduleInstances" ApiVersion = "beta" RemoveCmd = "Remove-PIMEntraRoleEligibleAssignment" SubscriptionBased = $false GraphBased = $true } } "Entra Role active" { $config = @{ ApiEndpoint = "https://graph.microsoft.com/beta/roleManagement/directory/roleAssignmentScheduleInstances" ApiVersion = "beta" RemoveCmd = "Remove-PIMEntraRoleActiveAssignment" SubscriptionBased = $false GraphBased = $true } } "Group eligible" { $config = @{ ApiEndpoint = "https://graph.microsoft.com/beta/roleManagement/directory/roleEligibilityScheduleInstances" ApiVersion = "beta" RemoveCmd = "Remove-PIMGroupEligibleAssignment" SubscriptionBased = $false GraphBased = $true GroupBased = $true } } "Group active" { $config = @{ ApiEndpoint = "https://graph.microsoft.com/beta/roleManagement/directory/roleAssignmentScheduleInstances" ApiVersion = "beta" RemoveCmd = "Remove-PIMGroupActiveAssignment" SubscriptionBased = $false GraphBased = $true GroupBased = $true } } default { throw "Unknown resource type: $ResourceType" } } # Justification filter used for identifying our assignments #$justificationFilter = "Invoke-EasyPIMOrchestrator" Write-Verbose "========== CONFIG ASSIGNMENTS DUMP ===========" foreach ($cfg in $ConfigAssignments) { $cfgId = $cfg.PrincipalId $cfgRole = $null foreach ($propName in @("RoleName", "Rolename", "Role", "roleName", "rolename", "role")) { if ($cfg.PSObject.Properties.Name -contains $propName) { $cfgRole = $cfg.$propName break } } $cfgScope = if ($null -ne $cfg.Scope) { $cfg.Scope } else { "NO_SCOPE" } Write-Verbose "Config Assignment: Principal=$cfgId, Role=$cfgRole, Scope=$cfgScope" } Write-Verbose "========== END CONFIG DUMP ===========" #endregion #region Process by resource type try { # Azure Resource roles if ($config.SubscriptionBased) { # Process each subscription foreach ($subscription in $ApiInfo.Subscriptions) { Write-Host " 🔍 Checking subscription: $subscription" -ForegroundColor White # Get current assignments $getCmd = if ($ResourceType -eq "Azure Role eligible") { "get-pimAzureResourceEligibleAssignment" } else { "get-pimAzureResourceActiveAssignment" } # Get assignments and process $allAssignments = & $getCmd -SubscriptionId $subscription -TenantId $ApiInfo.TenantId Write-Host " ├─ Found $($allAssignments.Count) total current assignments" -ForegroundColor White # Debug the first assignment to see its structure if ($allAssignments.Count -gt 0) { $firstAssignment = $allAssignments[0] Write-Verbose "DEBUG: First assignment properties: $($firstAssignment | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name)" Write-Verbose "DEBUG: First assignment: $($firstAssignment | ConvertTo-Json -Depth 2 -Compress)" } Write-Host "`n 📋 Processing assignments for: $ResourceType" -ForegroundColor Cyan # Process each assignment if ($allAssignments.Count -gt 0) { Write-Host "`n 📋 Analyzing assignments:" -ForegroundColor Cyan # Add a counter for processed assignments $processedCount = 0 foreach ($assignment in $allAssignments) { $processedCount++ # Extract assignment details - handle different property naming conventions # Try different property names based on API version $principalId = if ($null -ne $assignment.PrincipalId) { $assignment.PrincipalId } elseif ($null -ne $assignment.SubjectId) { $assignment.SubjectId } elseif ($null -ne $assignment.principalId) { $assignment.principalId } else { $null } $roleName = if ($null -ne $assignment.RoleDefinitionDisplayName) { $assignment.RoleDefinitionDisplayName } elseif ($null -ne $assignment.RoleName) { $assignment.RoleName } elseif ($null -ne $assignment.roleName) { $assignment.roleName } else { "Unknown" } $principalName = if ($null -ne $assignment.PrincipalDisplayName) { $assignment.PrincipalDisplayName } elseif ($null -ne $assignment.SubjectName) { $assignment.SubjectName } elseif ($null -ne $assignment.displayName) { $assignment.displayName } else { "Principal-$principalId" } # Different ways scope might be exposed $scope = if ($null -ne $assignment.ResourceId) { $assignment.ResourceId } elseif ($null -ne $assignment.scope) { $assignment.scope } elseif ($null -ne $assignment.Scope) { $assignment.Scope } elseif ($null -ne $assignment.directoryScopeId) { $assignment.directoryScopeId } else { $null } # Create a unique key to track this assignment $assignmentKey = "$principalId|$roleName|$scope" # Skip if we've already processed this assignment if ($processedAssignments.ContainsKey($assignmentKey)) { Write-Host " ├─ ⏭️ $principalName with role '$roleName' is a duplicate entry, skipping" -ForegroundColor DarkYellow $script:skipCounter++ Write-Verbose "Skipped duplicate assignment - counter now: $script:skipCounter" continue; } # Mark as processed $processedAssignments[$assignmentKey] = $true # For debugging property access issues if (-not $principalId -or -not $roleName) { Write-Host " ├─ ⚠️ Invalid assignment data, skipping" -ForegroundColor Yellow Write-Verbose "DEBUG: Invalid assignment: $($assignment | ConvertTo-Json -Depth 2 -Compress)" $script:skipCounter++ Write-Verbose "Skipped assignment - counter now: $script:skipCounter" continue } # Check if assignment matches config $foundInConfig = $false # Add detailed debug output Write-Verbose "Checking if assignment is in config: PrincipalId=$principalId, RoleName=$roleName, Scope=$scope" # Loop through each assignment in config foreach ($configAssignment in $ConfigAssignments) { # Get config role name with case-insensitive property lookup $configRole = $null foreach ($propName in @("RoleName", "Rolename", "Role", "roleName", "rolename", "role")) { if ($configAssignment.PSObject.Properties.Name -contains $propName) { $configRole = $configAssignment.$propName break } } # Get the principal ID with similar case-insensitive approach $configPrincipalId = $null foreach ($propName in @("PrincipalId", "principalId", "PrincipalID", "principalID")) { if ($configAssignment.PSObject.Properties.Name -contains $propName) { $configPrincipalId = $configAssignment.$propName break } } # Get the scope with case-insensitive approach $configScope = $null foreach ($propName in @("Scope", "scope")) { if ($configAssignment.PSObject.Properties.Name -contains $propName) { $configScope = $configAssignment.$propName break } } # Debug output to help diagnose matching issues Write-Verbose "Comparing assignment: Principal=$principalId, Role=$roleName, Scope=$scope" Write-Verbose "With config: Principal=$configPrincipalId, Role=$configRole, Scope=$configScope" # Check Principal ID match $principalMatches = $false # Direct PrincipalId match if ($configPrincipalId -eq $principalId) { $principalMatches = $true Write-Verbose "Principal matched directly" } # Check in PrincipalIds array if present elseif ($configAssignment.PSObject.Properties.Name -contains "PrincipalIds" -and $configAssignment.PrincipalIds -is [array]) { $principalMatches = $configAssignment.PrincipalIds -contains $principalId Write-Verbose "Principal checked against PrincipalIds array: $principalMatches" } # Check role name match - case insensitive comparison $roleMatches = $configRole -ieq $roleName # Check scope match directly $scopeMatches = $false if ($configScope) { # Only do direct scope comparison - no subscription ID extraction if ($configScope -eq $scope) { $scopeMatches = $true Write-Verbose "Scope exact match: $configScope" } # Handle empty scope by using subscription context elseif ([string]::IsNullOrEmpty($scope) -and $subscription) { $inferredScope = "/subscriptions/$subscription" if ($configScope -eq $inferredScope) { $scopeMatches = $true $scope = $inferredScope # Set for removal function Write-Verbose "Empty scope matched with inferred subscription scope: $inferredScope" } } } else { # If config has no scope, only match if assignment also has no scope $scopeMatches = [string]::IsNullOrEmpty($scope) Write-Verbose "Config has no scope, assignment scope is empty: $scopeMatches" } Write-Verbose "Match results: Principal=$principalMatches, Role=$roleMatches, Scope=$scopeMatches" # Match found if all three components match if ($principalMatches -and $roleMatches -and $scopeMatches) { $foundInConfig = $true Write-Verbose "✅ Match found in config!" break } } # Keep assignment if it's in config if ($foundInConfig) { Write-Host " ├─ ✅ $principalName with role '$roleName' matches config, keeping" -ForegroundColor Green $script:keptCounter++ Write-Verbose "Kept assignment - counter now: $script:keptCounter" continue } # Check if protected user if ($ProtectedUsers -contains $assignment.PrincipalId) { Write-Host " ├─ 🛡️ $principalName with role '$roleName' is a protected user, skipping" -ForegroundColor Yellow $protectedCounter++ # Only increment protected counter, not skip counter continue } # Check if protected role if ($script:protectedRoles -contains $roleName) { Write-host " ├─ ⚠️ $principalName with role '$roleName' is a protected role, skipping" $protectedCounter++ # Only increment protected counter continue } # Check if assignment is inherited - consolidate all checks $isInherited = $false # Create a tracking variable $inheritedReason = "" # Check for memberType property first if ($assignment.PSObject.Properties.Name -contains "memberType" -and $assignment.memberType -eq "Inherited") { $isInherited = $true $inheritedReason = "memberType=Inherited" } # Check for ScopeType property indicating management group elseif ($assignment.PSObject.Properties.Name -contains "ScopeType" -and $assignment.ScopeType -eq "managementgroup") { $isInherited = $true $inheritedReason = "ScopeType=managementgroup" } # Check for ScopeId indicating management group elseif ($assignment.PSObject.Properties.Name -contains "ScopeId" -and $assignment.ScopeId -like "*managementGroups*") { $isInherited = $true $inheritedReason = "ScopeId contains managementGroups" } if ($isInherited) { Write-Host " ├─ ⏭️ $principalName with role '$roleName' is an inherited assignment ($inheritedReason), skipping" -ForegroundColor DarkYellow $script:skipCounter++ Write-Verbose "Skipped assignment - counter now: $script:skipCounter" continue } # Remove assignment Write-Host " ├─ ❓ $principalName with role '$roleName' not in config, removing..." -ForegroundColor White # Prepare parameters for removal - use the exact scope value $removeParams = @{ tenantID = $ApiInfo.TenantId principalId = $principalId roleName = $roleName } # Only add scope if it's not empty if (-not [string]::IsNullOrEmpty($scope)) { $removeParams.scope = $scope } # Skip sensitive roles if ($roleName -eq "User Access Administrator") { Write-Warning " │ └─ ⚠️ Skipping removal of sensitive role: User Access Administrator" $script:skipCounter++ Write-Verbose "Skipped assignment - counter now: $script:skipCounter" continue } # Remove the assignment if ($PSCmdlet.ShouldProcess("Remove $ResourceType assignment for $principalName with role '$roleName'")) { try { Write-Verbose "Attempting to remove $principalName with role '$roleName'" # The Remove-* command might output "SUCCESS : Assignment removed!" directly # Capture both the output and the actual return value $outputLines = New-Object System.Collections.ArrayList $result = & $config.RemoveCmd @removeParams 2>&1 | ForEach-Object { $outputLines.Add($_) | Out-Null $_ } # Look for direct error objects in the result $hasError = $false if ($result -is [System.Management.Automation.ErrorRecord]) { $hasError = $true Write-Warning " │ └─ ⚠️ Removal failed: $($result.Exception.Message)" } elseif ($result.PSObject.Properties.Name -contains "error" -and $null -ne $result.error) { $hasError = $true $errorMessage = if ($result.error.PSObject.Properties.Name -contains 'message' -and $result.error.message) { $result.error.message } else { 'Unknown error' } Write-Warning " │ └─ ⚠️ Removal failed: $errorMessage" } # Check for "SUCCESS" in the output string itself $successMessage = $outputLines | Where-Object { $_ -match "SUCCESS" } if ($successMessage -and -not $hasError) { # Verify removal actually worked (optional for safety) $script:removeCounter++ Write-Verbose "Removed assignment - counter now: $script:removeCounter" Write-Host " │ └─ 🗑️ Removed successfully" -ForegroundColor Green } else { $script:skipCounter++ Write-Verbose "Skipped assignment - counter now: $script:skipCounter" } } catch { # Check for inheritance-related errors if ($_.Exception.Message -match "InsufficientPermissions|inherited|cannot delete|does not belong") { Write-Warning " │ └─ ⚠️ Cannot remove: $($_.Exception.Message)" $script:skipCounter++ Write-Verbose "Skipped assignment - counter now: $script:skipCounter" } else { Write-Error " │ └─ ❌ Failed to remove: $_" } } } else { $script:skipCounter++ Write-Verbose "Skipped assignment - counter now: $script:skipCounter" Write-Host " │ └─ ⏭️ Removal skipped (WhatIf mode)" -ForegroundColor DarkYellow } } } } } # Entra ID roles elseif ($config.GraphBased -and -not $config.GroupBased) { Write-Host " 🔍 Checking Entra roles" -ForegroundColor White # Query MS Graph for assignments with simplified error handling try { # Get directory roles for name resolution $directoryRoles = (Invoke-Graph -endpoint "/directoryRoles" -Method Get).value $roleTemplates = (Invoke-Graph -endpoint "/directoryRoleTemplates" -Method Get).value # Get instances (current assignments) $instancesEndpoint = ($config.ApiEndpoint -replace "https://graph.microsoft.com/beta", "") $allInstances = (Invoke-Graph -endpoint $instancesEndpoint -Method Get).value Write-Host " ├─ Found $($allInstances.Count) active instances (current assignments)" -ForegroundColor White Write-Host "`n 📋 Processing assignments for: $ResourceType" -ForegroundColor Cyan # Process each assignment if ($allInstances.Count -gt 0) { Write-Host "`n 📋 Analyzing assignments:" -ForegroundColor Cyan foreach ($assignment in $allInstances) { $principalId = $assignment.principalId $roleDefinitionId = $assignment.roleDefinitionId # Lookup role name from directory roles $roleName = "Unknown Role" $role = $directoryRoles | Where-Object { $_.id -eq $roleDefinitionId } | Select-Object -First 1 if ($role) { $roleName = $role.displayName } else { $template = $roleTemplates | Where-Object { $_.id -eq $roleDefinitionId } | Select-Object -First 1 if ($template) { $roleName = $template.displayName } } # Get principal name $principalName = "Principal-$principalId" try { $principalObj = Invoke-Graph -endpoint "/directoryObjects/$principalId" -Method Get -ErrorAction SilentlyContinue if ($principalObj.displayName) { $principalName = $principalObj.displayName } } catch { Write-Error " │ └─ ❌ Failed to resolve principal name for ID ${principalId}: $_" } # Check if assignment matches config $foundInConfig = $false foreach ($configAssignment in $ConfigAssignments) { $matchesPrincipal = $configAssignment.PrincipalId -eq $principalId $matchesRole = $configAssignment.Rolename -ieq $roleName if ($matchesPrincipal -and $matchesRole) { $foundInConfig = $true break } } # Keep assignment if it's in config if ($foundInConfig) { Write-Host " ├─ ✅ $principalName with role '$roleName' matches config, keeping" -ForegroundColor Green $script:keptCounter++ Write-Verbose "Kept assignment - counter now: $script:keptCounter" continue } # Check if protected user if ($ProtectedUsers -contains $principalId) { Write-Host " ├─ 🛡️ $principalName with role '$roleName' is a protected user, skipping" -ForegroundColor Yellow $protectedCounter++ # Only increment protected counter, not skip counter continue } # Check if protected role if ($script:protectedRoles -contains $roleName) { Write-Host " ├─ ⚠️ $principalName with role '$roleName' is a protected role, skipping" -ForegroundColor Yellow $protectedCounter++ # Only increment protected counter continue } # Remove assignment Write-Host " ├─ ❓ $principalName with role '$roleName' not in config, removing..." -ForegroundColor White # Prepare parameters for removal $removeParams = @{ tenantID = $ApiInfo.TenantId principalId = $principalId roleName = $roleName } # Skip sensitive roles if ($roleName -eq "User Access Administrator") { Write-Warning " │ └─ ⚠️ Skipping removal of sensitive role: User Access Administrator" $script:skipCounter++ Write-Verbose "Skipped assignment - counter now: $script:skipCounter" continue } # Remove the assignment if ($PSCmdlet.ShouldProcess("Remove $ResourceType assignment for $principalName with role '$roleName'")) { try { $result = & $config.RemoveCmd @removeParams $script:removeCounter++ Write-Verbose "Removed assignment - counter now: $script:removeCounter" Write-Host " │ └─ 🗑️ Removed successfully" -ForegroundColor Green } catch { # Check for inheritance or permission errors if ($_.Exception.Message -match "InsufficientPermissions" -or $_.Exception.Message -match "inherited" -or $_.Exception.Message -match "cannot delete an assignment" -or $_.Exception.Message -match "does not belong") { Write-Warning " │ └─ ⚠️ Cannot remove: Assignment appears to be inherited from a higher scope" $script:skipCounter++ Write-Verbose "Skipped assignment - counter now: $script:skipCounter" } else { Write-Error " │ └─ ❌ Failed to remove: $_" } } } else { $script:skipCounter++ Write-Verbose "Skipped assignment - counter now: $script:skipCounter" Write-Host " │ └─ ⏭️ Removal skipped (WhatIf mode)" -ForegroundColor DarkYellow } } } } catch { if ($_.Exception.Message -match "Permission") { Write-Warning "⚠️ Insufficient permissions to manage Entra role assignments." Write-Warning "Required permissions: RoleEligibilitySchedule.ReadWrite.Directory, RoleManagement.ReadWrite.Directory" } else { Write-Error "Failed to query Entra role assignments: $_" } } } # Group roles elseif ($config.GraphBased -and $config.GroupBased) { Write-Host " ⚠️ $ResourceType cleanup via Graph API - Not yet implemented" -ForegroundColor Yellow } } catch { Write-Error "An error occurred processing $ResourceType cleanup: $_" } #endregion #region Summary # Use Write-Host instead of Write-Output for consistent display Write-Host "`n┌────────────────────────────────────────────────────┐" -ForegroundColor Cyan Write-Host "│ $ResourceType Cleanup Summary" -ForegroundColor Cyan Write-Host "├────────────────────────────────────────────────────┤" -ForegroundColor Cyan Write-Host "│ ✅ Kept: $script:keptCounter" -ForegroundColor White Write-Host "│ 🗑️ Removed: $script:removeCounter" -ForegroundColor White Write-Host "│ ⏭️ Skipped: $script:skipCounter" -ForegroundColor White if ($protectedCounter -gt 0) { Write-Host "│ 🛡️ Protected: $protectedCounter" -ForegroundColor White } Write-Host "└────────────────────────────────────────────────────┘" -ForegroundColor Cyan #endregion # Update reference parameters at the end if ($KeptCounter) { $KeptCounter.Value = $script:keptCounter } if ($RemoveCounter) { $RemoveCounter.Value = $script:removeCounter } if ($SkipCounter) { $SkipCounter.Value = $script:skipCounter } # Return details object return @{ ResourceType = $ResourceType KeptCount = $script:keptCounter RemovedCount = $script:removeCounter SkippedCount = $script:skipCounter ProtectedCount = $protectedCounter } } function Invoke-InitialCleanup { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] [OutputType([System.Collections.Hashtable])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] param( [Parameter(Mandatory = $true)] [object]$Config, [Parameter(Mandatory = $true)] [string]$TenantId, [Parameter(Mandatory = $true)] [string]$SubscriptionId, # These parameters exist for future extension but aren't currently used directly # We'll add the suppression attribute to avoid PSScriptAnalyzer warnings [Parameter(Mandatory = $false)] [array]$AzureRoles = @(), [Parameter(Mandatory = $false)] [array]$AzureRolesActive = @(), [Parameter(Mandatory = $false)] [array]$EntraRoles = @(), [Parameter(Mandatory = $false)] [array]$EntraRolesActive = @(), [Parameter(Mandatory = $false)] [array]$GroupRoles = @(), [Parameter(Mandatory = $false)] [array]$GroupRolesActive = @(), [Parameter(Mandatory = $false)] [ref]$KeptCounter, [Parameter(Mandatory = $false)] [ref]$RemoveCounter, [Parameter(Mandatory = $false)] [ref]$SkipCounter ) # Display initial warning about potentially dangerous operation Write-Warning "⚠️ CAUTION: POTENTIALLY DESTRUCTIVE OPERATION ⚠️" Write-Warning "This will remove ALL PIM assignments not defined in your configuration." Write-Warning "If your protected users list is incomplete, you may lose access to critical resources!" Write-Warning "Protected users count: $($Config.ProtectedUsers.Count)" Write-Warning "---" Write-Warning "USAGE GUIDANCE:" Write-Warning "• To preview changes without making them: Use -WhatIf" Write-Warning '• To skip confirmation prompts: Use -Confirm:$false' Write-Warning '• Example: Invoke-InitialCleanup ... -Confirm:$false' Write-Warning "---" # Global confirmation for the entire operation $operationDescription = "Initial cleanup mode - remove ALL assignments not in configuration" $operationTarget = "PIM assignments across Azure, Entra ID, and Groups" if (-not $PSCmdlet.ShouldProcess($operationTarget, $operationDescription)) { Write-Output "Operation cancelled by user." return } Write-SectionHeader "Initial Mode Cleanup" Write-StatusInfo "This will remove all assignments not in the configuration except for protected users" # Track overall statistics $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() # Initialize standardized total counters $totalKept = 0 $totalRemoved = 0 $totalProtected = 0 # Initialize protected users list $protectedUsers = @($Config.ProtectedUsers) Write-StatusInfo "Found $($protectedUsers.Count) protected users that will not be removed" # Define protected roles that should never be removed automatically $protectedRoles = @( "User Access Administrator", "Global Administrator", "Privileged Role Administrator", "Security Administrator" ) # Helper functions for cleaner code function Test-IsProtectedAssignment { param ([string]$PrincipalId) return $protectedUsers -contains $PrincipalId } function Test-AzureRoleAssignmentInConfig { param ( [string]$PrincipalId, [string]$RoleName, [string]$Scope, [array]$ConfigAssignments ) foreach ($config in $ConfigAssignments) { $roleMatches = ($config.Rolename -eq $RoleName) -or ($config.Role -eq $RoleName) # Check for direct PrincipalId match $principalMatches = $config.PrincipalId -eq $PrincipalId # Also check in PrincipalIds array if present if (-not $principalMatches -and $config.PSObject.Properties.Name -contains "PrincipalIds") { $principalMatches = $config.PrincipalIds -contains $PrincipalId } if ($principalMatches -and $roleMatches -and $config.Scope -eq $Scope) { return $true } } return $false } function Test-EntraRoleAssignmentInConfig { param ( [string]$PrincipalId, [string]$RoleName, [array]$ConfigAssignments ) foreach ($config in $ConfigAssignments) { $roleNameMatch = $config.Rolename -eq $RoleName # Check direct PrincipalId $principalMatch = $config.PrincipalId -eq $PrincipalId # Check PrincipalIds array if (-not $principalMatch -and $config.PSObject.Properties.Name -contains "PrincipalIds") { $principalMatch = $config.PrincipalIds -contains $PrincipalId } if ($principalMatch -and $roleNameMatch) { return $true } } return $false } function Test-GroupRoleAssignmentInConfig { param ( [string]$PrincipalId, [string]$RoleName, [string]$GroupId, [array]$ConfigAssignments ) foreach ($config in $ConfigAssignments) { $roleNameMatch = $config.Rolename -eq $RoleName $groupIdMatch = $config.GroupId -eq $GroupId # Check direct PrincipalId $principalMatch = $config.PrincipalId -eq $PrincipalId # Check PrincipalIds array if (-not $principalMatch -and $config.PSObject.Properties.Name -contains "PrincipalIds") { $principalMatch = $config.PrincipalIds -contains $PrincipalId } if ($principalMatch -and $roleNameMatch -and $groupIdMatch) { return $true } } return $false } function Invoke-CleanupAzureRoles { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param ( [string]$Type, [array]$ConfigAssignments, [string]$GetCommand, [string]$RemoveCommand ) Write-SubHeader "Azure Role $Type Assignments Cleanup" $removeCounter = 0 $skipCounter = 0 $protectedCounter = 0 $sectionWatch = [System.Diagnostics.Stopwatch]::StartNew() # Get all existing assignments Write-StatusInfo "Fetching existing $Type assignments..." $existing = & $GetCommand -tenantID $TenantId -subscriptionID $SubscriptionId # Filter to only include direct assignments $existing = $existing | Where-Object { $_.memberType -eq "Direct" } Write-StatusInfo "Found $($existing.Count) direct $Type assignments to process" # Process in batches for better performance reporting $total = $existing.Count $processed = 0 foreach ($assignment in $existing) { $processed++ $principalId = $assignment.PrincipalId $roleName = $assignment.RoleName $scope = $assignment.ScopeId # Check if principal exists if (-not (Test-PrincipalExists -PrincipalId $principalId)) { Write-StatusWarning "Principal $principalId does not exist, skipping..." continue } $percentComplete = [Math]::Floor(($processed / $total) * 100) Write-Progress -Activity "Processing Azure Role $Type Assignments" -Status "$processed of $total ($percentComplete%)" -PercentComplete $percentComplete # Check if assignment is in config $isInConfig = Test-AzureRoleAssignmentInConfig -PrincipalId $principalId -RoleName $roleName -Scope $scope -ConfigAssignments $ConfigAssignments if (-not $isInConfig) { # Check if role is protected if ($protectedRoles -contains $roleName) { Write-Output " ├─ ⚠️ $principalId with role '$roleName' is a protected role, skipping" continue } # Check if principal is protected if (Test-IsProtectedAssignment -PrincipalId $principalId) { Write-StatusInfo "Skipping removal of protected user $principalId with role $roleName on scope $scope" $protectedCounter++ continue } # Not in config and not protected, so remove $actionDescription = "Remove Azure Role $Type assignment for $principalId with role $roleName on scope $scope" if ($PSCmdlet.ShouldProcess($actionDescription)) { try { Write-StatusProcessing "Removing assignment for $principalId with role $roleName..." & $RemoveCommand -tenantID $TenantId -scope $scope -principalId $principalId -roleName $roleName Write-StatusSuccess "Successfully removed assignment" $removeCounter++ } catch { Write-StatusError "Failed to remove assignment: $_" $skipCounter++ } } } else { Write-Verbose "Assignment in config, keeping: $principalId, $roleName, $scope" $skipCounter++ } } Write-Progress -Activity "Processing Azure Role $Type Assignments" -Completed $elapsed = $sectionWatch.Elapsed.TotalSeconds Write-StatusInfo "Completed in $elapsed seconds" Write-Summary -Category "Azure Role $Type Cleanup" -Created $skipCounter -Removed $removeCounter -Failed 0 -OperationType "Cleanup" Write-StatusInfo "Protected assignments skipped: $protectedCounter" # Return standardized result object return @{ KeptCount = $skipCounter RemovedCount = $removeCounter ProtectedCount = $protectedCounter } } function Invoke-CleanupEntraIDRoles { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param ( [string]$Type, [array]$ConfigAssignments, [string]$GetCommand, [string]$RemoveCommand ) Write-SubHeader "Entra ID Role $Type Assignments Cleanup" $removeCounter = 0 $skipCounter = 0 $protectedCounter = 0 $sectionWatch = [System.Diagnostics.Stopwatch]::StartNew() # Get ALL existing assignments at once Write-StatusInfo "Fetching ALL existing Entra ID $Type assignments..." $allExisting = & $GetCommand -tenantID $TenantId Write-StatusInfo "Found $($allExisting.Count) existing assignments" # Filter direct assignments $existing = $allExisting | Where-Object { $_.memberType -eq "Direct" } Write-StatusInfo "Found $($existing.Count) direct $Type assignments to process" # Process in batches $total = $existing.Count $processed = 0 foreach ($assignment in $existing) { $processed++ $principalId = $assignment.PrincipalId $roleName = $assignment.RoleName # Check if principal exists if (-not (Test-PrincipalExists -PrincipalId $principalId)) { Write-StatusWarning "Principal $principalId does not exist, skipping..." continue } $percentComplete = [Math]::Floor(($processed / $total) * 100) Write-Progress -Activity "Processing Entra ID Role $Type Assignments" -Status "$processed of $total ($percentComplete%)" -PercentComplete $percentComplete # Check if assignment is in config $isInConfig = Test-EntraRoleAssignmentInConfig -PrincipalId $principalId -RoleName $roleName -ConfigAssignments $ConfigAssignments if (-not $isInConfig) { # Check if role is protected if ($protectedRoles -contains $roleName) { Write-Output " ├─ ⚠️ $principalId with role '$roleName' is a protected role, skipping" continue } # Check if principal is protected if (Test-IsProtectedAssignment -PrincipalId $principalId) { Write-StatusInfo "Skipping removal of protected user $principalId with role $roleName" $protectedCounter++ continue } # Not in config and not protected, so remove $actionDescription = "Remove Entra ID Role $Type assignment for $principalId with role $roleName" if ($PSCmdlet.ShouldProcess($actionDescription)) { try { Write-StatusProcessing "Removing assignment for $principalId with role $roleName..." & $RemoveCommand -tenantID $TenantId -principalId $principalId -roleName $roleName Write-StatusSuccess "Successfully removed assignment" $removeCounter++ } catch { Write-StatusError "Failed to remove assignment: $_" $skipCounter++ } } } else { Write-Verbose "Assignment in config, keeping: $principalId, $roleName" $skipCounter++ } } Write-Progress -Activity "Processing Entra ID Role $Type Assignments" -Completed $elapsed = $sectionWatch.Elapsed.TotalSeconds Write-StatusInfo "Completed in $elapsed seconds" Write-Summary -Category "Entra ID Role $Type Cleanup" -Kept $skipCounter -Removed $removeCounter -Skipped $protectedCounter -OperationType "Cleanup" Write-StatusInfo "Protected assignments skipped: $protectedCounter" # Return standardized result object return @{ KeptCount = $skipCounter RemovedCount = $removeCounter ProtectedCount = $protectedCounter } } function Invoke-CleanupGroupRoles { [CmdletBinding(SupportsShouldProcess = $true)] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param ( [string]$Type, [array]$ConfigAssignments, [string]$GetCommand, [string]$RemoveCommand, [ref]$TotalKept, [ref]$TotalRemoved, [ref]$TotalSkipped ) # Local counters for function summary $removeCounter = 0 $skipCounter = 0 $protectedCounter = 0 # Add before these lines in the Group assignment verification section: $assignmentExists = $false foreach ($existing in $currentAssignments) { if (($existing.PrincipalId -eq $params['principalID']) -and ($existing.Type -eq $params['type'] -or $existing.RoleName -eq $params['type'])) { # Display match info in normal output $matchInfo = "principalId='$($existing.PrincipalId)' and memberType='$($existing.Type -or $existing.RoleName)'" Write-Host " │ ├─ 🔍 Match found: $matchInfo" -ForegroundColor Cyan $assignmentExists = $true break } } # Rest of the function code stays the same # At the end, update the summary to use correct variables Write-Host "│ ✅ Kept: $skipCounter" -ForegroundColor White Write-Host "│ 🗑️ Removed: $removeCounter" -ForegroundColor White Write-Host "│ ⏭️ Skipped: $protectedCounter" -ForegroundColor White # Update the reference parameters directly $TotalKept.Value += $skipCounter $TotalRemoved.Value += $removeCounter $TotalSkipped.Value += $protectedCounter } # Execute cleanup operations if ($Config.AzureRoles -or $AzureRoles.Count -gt 0) { $roleAssignments = if ($Config.AzureRoles) { $Config.AzureRoles } else { $AzureRoles } $result = Invoke-CleanupAzureRoles -Type "Eligible" -ConfigAssignments $roleAssignments -GetCommand "Get-PIMAzureResourceEligibleAssignment" -RemoveCommand "Remove-PIMAzureResourceEligibleAssignment" $totalKept += $result.KeptCount $totalRemoved += $result.RemovedCount $totalProtected += $result.ProtectedCount } if ($Config.AzureRolesActive -or $AzureRolesActive.Count -gt 0) { $roleAssignments = if ($Config.AzureRolesActive) { $Config.AzureRolesActive } else { $AzureRolesActive } $result = Invoke-CleanupAzureRoles -Type "Active" -ConfigAssignments $roleAssignments -GetCommand "Get-PIMAzureResourceActiveAssignment" -RemoveCommand "Remove-PIMAzureResourceActiveAssignment" $totalKept += $result.KeptCount $totalRemoved += $result.RemovedCount $totalProtected += $result.ProtectedCount } if ($Config.EntraIDRoles -or $EntraRoles.Count -gt 0) { $roleAssignments = if ($Config.EntraIDRoles) { $Config.EntraIDRoles } else { $EntraRoles } $result = Invoke-CleanupEntraIDRoles -Type "Eligible" -ConfigAssignments $roleAssignments -GetCommand "Get-PIMEntraRoleEligibleAssignment" -RemoveCommand "Remove-PIMEntraRoleEligibleAssignment" $totalKept += $result.KeptCount $totalRemoved += $result.RemovedCount $totalProtected += $result.ProtectedCount } if ($Config.EntraIDRolesActive -or $EntraRolesActive.Count -gt 0) { $roleAssignments = if ($Config.EntraIDRolesActive) { $Config.EntraIDRolesActive } else { $EntraRolesActive } $result = Invoke-CleanupEntraIDRoles -Type "Active" -ConfigAssignments $roleAssignments -GetCommand "Get-PIMEntraRoleActiveAssignment" -RemoveCommand "Remove-PIMEntraRoleActiveAssignment" $totalKept += $result.KeptCount $totalRemoved += $result.RemovedCount $totalProtected += $result.ProtectedCount } # Group role cleanup functionality is currently disabled # There is no Get-PIMGroup cmdlet available to retrieve all PIM-enabled groups <# if ($Config.GroupRoles -or $GroupRoles.Count -gt 0) { $roleAssignments = if ($Config.GroupRoles) { $Config.GroupRoles } else { $GroupRoles } Invoke-CleanupGroupRoles -Type "Eligible" -ConfigAssignments $roleAssignments -GetCommand "Get-PIMGroupEligibleAssignment" -RemoveCommand "Remove-PIMGroupEligibleAssignment" -TotalKept ([ref]$script:totalSkipped) -TotalRemoved ([ref]$script:totalRemoved) -TotalSkipped ([ref]$script:totalProtected) } if ($Config.GroupRolesActive -or $GroupRolesActive.Count -gt 0) { $roleAssignments = if ($Config.GroupRolesActive) { $Config.GroupRolesActive } else { $GroupRolesActive } Invoke-CleanupGroupRoles -Type "Active" -ConfigAssignments $roleAssignments -GetCommand "Get-PIMGroupActiveAssignment" -RemoveCommand "Remove-PIMGroupActiveAssignment" } #> # Note: We're keeping the Invoke-CleanupGroupRoles function defined for future use # when an appropriate method becomes available to enumerate all PIM-enabled groups $totalTime = $stopwatch.Elapsed.TotalMinutes # Final summary Write-SectionHeader "Initial Mode Cleanup Summary" Write-StatusInfo "Total assignments removed: $totalRemoved" Write-StatusInfo "Total assignments kept: $totalKept" Write-StatusInfo "Total protected assignments skipped: $totalProtected" Write-StatusInfo "Total execution time: $($totalTime.ToString("F2")) minutes" Write-StatusSuccess "Initial mode cleanup completed" # Update reference parameters if ($KeptCounter) { $KeptCounter.Value = $totalKept } if ($RemoveCounter) { $RemoveCounter.Value = $totalRemoved } if ($SkipCounter) { $SkipCounter.Value = $totalProtected } # Return standardized result object return @{ KeptCount = $totalKept RemovedCount = $totalRemoved SkippedCount = $totalProtected # Using 'SkippedCount' for backward compatibility } } function Invoke-ResourceAssignments { [CmdletBinding(SupportsShouldProcess = $true)] [OutputType([System.Collections.Hashtable])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param ( [string]$ResourceType, [array]$Assignments, [hashtable]$CommandMap, [PSCustomObject]$Config ) # Improved formatting for section headers Write-Host "`n┌────────────────────────────────────────────────────┐" Write-Host "│ Processing $ResourceType Assignments" Write-Host "└────────────────────────────────────────────────────┘`n" write-host " 🔍 Analyzing configuration" write-host " ├─ Found $($Assignments.Count) assignments in config" $createCounter = 0 $skipCounter = 0 $errorCounter = 0 # Get existing assignments try { $cmd = $CommandMap.GetCmd $params = $CommandMap.GetParams $existingAssignments = & $cmd @params write-host " └─ Found $($existingAssignments.Count) existing assignments" } catch { write-host " └─ ⚠️ Error fetching existing assignments: $_" $existingAssignments = @() } # Ensure existingAssignments is always an array if ($null -eq $existingAssignments) { Write-Verbose "Command returned null result, initializing empty array" $existingAssignments = @() } elseif (-not ($existingAssignments -is [array])) { Write-Verbose "Command returned a single object, converting to array" $existingAssignments = @($existingAssignments) } # Add debug output for assignments if ($Assignments.Count -gt 0) { write-host "`n 📋 Processing assignments:" write-host " ├─ Found $($Assignments.Count) assignments to process" # Display details for ALL assignments foreach ($assignment in $Assignments) { $principalId = $assignment.PrincipalId $roleName = if ([string]::IsNullOrEmpty($assignment.Rolename)) { $assignment.Role } else { $assignment.Rolename } # Fix scope display - different formats for different resource types $scopeDisplay = "" if ($ResourceType -like "Azure Role*" -and $assignment.Scope) { $scopeDisplay = " on scope $($assignment.Scope)" } elseif ($ResourceType -like "Group*" -and $assignment.GroupId) { $scopeDisplay = " in group $($assignment.GroupId)" } # For Entra ID roles, no scope is needed try { $principalName = (Get-MgUser -UserId $principalId -ErrorAction SilentlyContinue).DisplayName if (-not $principalName) { $principalName = "Principal-$principalId" } } catch { $principalName = "Principal-$principalId" } write-host " ├─ Processing: $principalName with role '$roleName'$scopeDisplay" } Write-Verbose "Debug: Total assignments to process: $($Assignments.Count)" # Display first few assignments in verbose mode foreach ($a in $Assignments | Select-Object -First 3) { Write-Verbose "Debug: Assignment: $($a | ConvertTo-Json -Compress)" } } foreach ($assignment in $Assignments) { # Extract identifiable information for display $principalId = $assignment.PrincipalId if (-not $principalId) { Write-Warning "Assignment is missing PrincipalId: $($assignment | ConvertTo-Json -Compress)" $errorCounter++ continue } $roleName = if ([string]::IsNullOrEmpty($assignment.Rolename)) { $assignment.Role } else { $assignment.Rolename } $principalName = "Principal-$principalId" # Try to get a better name for the principal if possible try { $principalObj = Get-AzADUser -ObjectId $principalId -ErrorAction SilentlyContinue if ($principalObj) { $principalName = $principalObj.DisplayName } else { $principalGroup = Get-AzADGroup -ObjectId $principalId -ErrorAction SilentlyContinue if ($principalGroup) { $principalName = $principalGroup.DisplayName } } } catch { write-verbose " ├─ ⚠️ Failed to resolve principal name for ID ${principalId}: $_" # Silently continue with the default name } # Check if principal exists (if not already done in the command map) if (-not $CommandMap.DirectFilter) { if (-not (Test-PrincipalExists -PrincipalId $assignment.PrincipalId)) { write-host " ├─ ❌ $principalName does not exist, skipping assignment" $errorCounter++ continue } } # Check if group exists for Group Role assignments if ($ResourceType -like "Group Role*") { $groupId = $assignment.GroupId if (-not $groupId) { Write-Host " ├─ ❌ Missing GroupId for assignment, skipping" -ForegroundColor Red $errorCounter++ continue } # Validate the group exists try { $uri = "https://graph.microsoft.com/v1.0/directoryObjects/$groupId" Invoke-MgGraphRequest -Uri $uri -Method GET -ErrorAction Stop Write-Verbose "Group $groupId exists and is accessible" # Check if group is eligible for PIM (not synced from on-premises) if (-not (Test-GroupEligibleForPIM -GroupId $groupId)) { Write-Host " ├─ ⚠️ Group $groupId is not eligible for PIM management (likely synced from on-premises), skipping" -ForegroundColor Yellow $skipCounter++ continue } } catch { Write-StatusWarning "Group $groupId does not exist, skipping all assignments" $results.Skipped += $assignmentsForGroup.Count # Skipped rather than Failed continue } } # Scope information for display $scopeInfo = if ($ResourceType -like "Azure Role*" -and $assignment.Scope) { " on scope $($assignment.Scope)" } elseif ($ResourceType -like "Group*" -and $assignment.GroupId) { " in group $($assignment.GroupId)" } else { "" } # Display assignment being processed Write-Host " ├─ 🔍 $principalName with role '$roleName'$scopeInfo" # Initialize matchInfo variable at the beginning of the foreach loop $matchInfo = "unknown reason" # Initialize with default value # Check if assignment already exists $found = 0 foreach ($existing in $existingAssignments) { # Add debug output to see what we're comparing Write-Verbose "Comparing with existing: $($existing | ConvertTo-Json -Depth 10 -ErrorAction SilentlyContinue)" # Different comparison for different resource types if ($ResourceType -like "Entra ID Role*") { # Debug: Show the first existing assignment structure to help us understand it if ($existingAssignments.Count -gt 0 -and $existing -eq $existingAssignments[0]) { Write-Verbose "First Entra ID existing assignment structure:" Write-Verbose ($existing | ConvertTo-Json -Depth 10 -ErrorAction SilentlyContinue) } # The issue is that Graph API properties might have different casing or structure # Try multiple property paths for more robust comparison # Check if we're dealing with an expanded object or a basic one if ($existing.PSObject.Properties.Name -contains 'principal') { Write-Verbose "Comparing expanded Entra object: principal.id=$($existing.principal.id) to $($assignment.PrincipalId)" Write-Verbose "Comparing expanded Entra object: roleDefinition.displayName=$($existing.roleDefinition.displayName) to $roleName" # Case-insensitive comparison for role names if (($existing.principal.id -eq $assignment.PrincipalId) -and ($existing.roleDefinition.displayName -ieq $roleName)) { $found = 1 Write-Verbose "Match found using Entra ID expanded object comparison" break } } else { # Try standard properties with case-insensitive role name comparison Write-Verbose "Comparing standard Entra object: PrincipalId=$($existing.PrincipalId) to $($assignment.PrincipalId)" Write-Verbose "Comparing standard Entra object: RoleName=$($existing.RoleName) to $roleName" if (($existing.PrincipalId -eq $assignment.PrincipalId) -and ($existing.RoleName -ieq $roleName)) { $found = 1 Write-Verbose "Match found using Entra ID standard comparison" break } } } elseif ($ResourceType -like "Group Role*") { # Debug the first group assignment structure if ($existingAssignments.Count -gt 0 -and $existing -eq $existingAssignments[0]) { Write-Verbose "First Group Role existing assignment structure:" Write-Verbose ($existing | ConvertTo-Json -Depth 10 -ErrorAction SilentlyContinue) } # For Group roles, we need to check PrincipalId/ID and Type/type $principalMatched = $false $roleMatched = $false # Simplified principal ID check - just check for the two common property names if ($existing.PrincipalId -eq $assignment.PrincipalId -or $existing.principalid -eq $assignment.PrincipalId) { $principalMatched = $true Write-Verbose "Principal ID matched in group assignment" } # Simplified role name/type check - check only the different property names if ($existing.RoleName -ieq $roleName -or $existing.Type -ieq $roleName -or $existing.memberType -ieq $roleName) { $roleMatched = $true Write-Verbose "Role/type matched in group assignment (property: $($existing.PSObject.Properties.Name -like '*type*' -or $existing.PSObject.Properties.Name -like '*role*'))" } # Match found if both principal and role matched if ($principalMatched -and $roleMatched) { $found = 1 # Store information about why this matched for display later $matchReason = if ($null -ne $existing.memberType) { "memberType='$($existing.memberType)'" } elseif ($null -ne $existing.Type) { "type='$($existing.Type)'" } else { "role matched" } $matchInfo = "principalId='$($existing.principalId)' and $matchReason" Write-host "Match found for Group Role assignment: $matchInfo" break } } else { # Standard comparison for Azure roles and others if (($existing.PrincipalId -eq $assignment.PrincipalId) -and ($existing.RoleName -eq $roleName)) { $found = 1 break } } } if ($found -eq 0) { # Create a SINGLE parameters hashtable - this is critical $params = @{} # First, copy all base parameters from the command map if ($CommandMap.CreateParams) { foreach ($key in $CommandMap.CreateParams.Keys) { $params[$key] = $CommandMap.CreateParams[$key] } } # Ensure justification exists from the beginning if (-not $params.ContainsKey('justification')) { # Try to get it from Config first if ($Config -and $Config.PSObject.Properties.Name -contains 'Justification' -and $Config.Justification) { $params['justification'] = $Config.Justification Write-Verbose "Using justification from Config: $($Config.Justification)" } # Otherwise generate a new one else { $params['justification'] = "Created by EasyPIM Orchestrator on $(Get-Date -Format 'yyyy-MM-dd')" Write-Verbose "Using default justification: $($params['justification'])" } } # Display justification Write-Verbose " │ ├─ 📝 Justification: $($params['justification'])" # Resource-specific parameters if ($ResourceType -like "Azure Role*") { $params['principalId'] = $assignment.PrincipalId $params['roleName'] = $roleName $params['scope'] = $assignment.Scope } elseif ($ResourceType -like "Group Role*") { # For Group roles, use uppercase ID properties $params['principalID'] = $assignment.PrincipalId # Capital ID $params['groupID'] = $assignment.GroupId # Capital ID $params['type'] = $roleName.ToLower() # Lowercase type # Double-check the parameters are set if (-not $params.ContainsKey('groupID') -or [string]::IsNullOrEmpty($params['groupID'])) { Write-Error "Failed to set groupID parameter" $errorCounter++ continue } } else { # For other resource types like Entra roles $params['principalId'] = $assignment.PrincipalId $params['roleName'] = $roleName } # Handle duration and permanent settings if ($assignment.Permanent -eq $true) { $params['permanent'] = $true Write-Host " │ ├─ ⏱️ Setting as permanent assignment" -ForegroundColor Cyan } elseif ($assignment.Duration) { $params['duration'] = $assignment.Duration Write-Host " │ ├─ ⏱️ Setting duration: $($assignment.Duration)" -ForegroundColor Cyan } else { Write-Host " │ ├─ ⏱️ Using maximum allowed duration" -ForegroundColor Cyan } # Pre-execution debug Write-Verbose "Command accepts these parameters: $((Get-Command $CommandMap.CreateCmd -ErrorAction SilentlyContinue).Parameters.Keys -join ', ')" Write-Verbose "Final command parameters:" Write-Verbose ($params | ConvertTo-Json -Compress) # IMPORTANT: Do not create any new parameter hashtables after this point # Action description for ShouldProcess $actionDescription = if ($ResourceType -like "Azure Role*") { "Create $ResourceType assignment for $principalName with role '$roleName' on scope $($assignment.Scope)" } else { "Create $ResourceType assignment for $principalName with role '$roleName'" } if ($PSCmdlet.ShouldProcess($actionDescription)) { try { # Execute the command $result = & $CommandMap.CreateCmd @params # For Group role assignments, verify the operation succeeded if ($ResourceType -like "Group Role*") { # Add verification that the assignment was created $verifyParams = @{ tenantID = $params['tenantID'] groupID = $params['groupID'] # Don't include principalID to get all assignments for the group } # Get command name based on eligible vs active $verifyCmd = if ($ResourceType -like "*eligible*") { "Get-PIMGroupEligibleAssignment" } else { "Get-PIMGroupActiveAssignment" } Write-Verbose "Verifying assignment creation with $verifyCmd" # Get current assignments and verify ours exists $currentAssignments = & $verifyCmd @verifyParams # Check if our assignment now exists $assignmentExists = $false foreach ($existing in $currentAssignments) { if (($existing.PrincipalId -eq $params['principalID']) -and ($existing.Type -eq $params['type'] -or $existing.RoleName -eq $params['type'])) { $assignmentExists = $true break } } if ($assignmentExists) { $createCounter++ Write-Host " │ └─ ✅ Created and verified successfully" -ForegroundColor Green } else { Write-Host " │ └─ ⚠️ Command completed but assignment not found in verification" -ForegroundColor Yellow $errorCounter++ } } # For Azure roles, we rely on the existing check elseif (($null -ne $result) -or ($ResourceType -like "Azure Role*" -and $? -eq $true)) { $createCounter++ Write-Host " │ └─ ✅ Created successfully" -ForegroundColor Green } else { # For other resource types Write-Host " │ └─ ⚠️ Command completed but returned null" -ForegroundColor Yellow $skipCounter++ } } catch { Write-Host " │ └─ ❌ Failed to create: $_" -ForegroundColor Red $errorCounter++ } } } else { # After the loop ends, update the else statement with more details: if ($ResourceType -like "Entra ID Role*") { Write-Host " │ └─ ⏭️ Assignment already exists in Entra ID, skipping" -ForegroundColor DarkYellow } elseif ($ResourceType -like "Group Role*") { Write-Host " │ └─ ⏭️ Assignment already exists as $matchInfo, skipping" -ForegroundColor DarkYellow } else { Write-Host " │ └─ ⏭️ Assignment already exists, skipping" -ForegroundColor DarkYellow } $skipCounter++ } } # Return the counters in a structured format Write-CreationSummary -Category "$ResourceType Assignments" -Created $createCounter -Skipped $skipCounter -Failed $errorCounter # Return standardized result return @{ Created = $createCounter Skipped = $skipCounter Failed = $errorCounter } } function Write-SectionHeader { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] param ([string]$Title) Write-Host "`n┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓" -ForegroundColor Cyan Write-Host "┃ $($Title.PadRight(76)) ┃" -ForegroundColor Cyan Write-Host "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛" -ForegroundColor Cyan } function Write-SubHeader { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] param ([string]$Title) Write-Host "`n▶ $Title" -ForegroundColor Yellow Write-Host "┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄" -ForegroundColor DarkGray } function Write-GroupHeader { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] param ([string]$Title) # Truncate title if it's too long if ($Title.Length -gt 65) { $Title = $Title.Substring(0, 62) + "..." } $remainingLength = [Math]::Max(0, (70 - $Title.Length)) Write-Host "`n┌─── $Title $("─" * $remainingLength)" -ForegroundColor Magenta } function Write-StatusSuccess { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] param ([string]$Message) Write-Host "✅ $Message" -ForegroundColor Green } function Write-StatusInfo { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] param ([string]$Message) Write-Host "ℹ️ $Message" -ForegroundColor Blue } function Write-StatusProcessing { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] param ([string]$Message) Write-Host "⚙️ $Message" -ForegroundColor Gray } function Write-StatusWarning { param ([string]$Message) Write-Warning "⚠️ $Message" } function Write-StatusError { param ([string]$Message) Write-Error "❌ $Message" } function Write-Summary { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] param ( [string]$Category, [int]$Created = 0, [int]$Removed = 0, [int]$Skipped = 0, [int]$Failed = 0, [int]$Protected = 0, [ValidateSet("Creation", "Cleanup")] [string]$OperationType = "Creation" ) Write-Host "`n┌───────────────────────────────────────────────────────────────────────────────┐" -ForegroundColor White Write-Host "│ SUMMARY: $Category" -ForegroundColor White Write-Host "├───────────────────────────────────────────────────────────────────────────────┤" -ForegroundColor White if ($OperationType -eq "Cleanup") { # Use the right labels for cleanup operations Write-Host "│ ✅ Kept : $Created" -ForegroundColor White # Reuse Created parameter for kept Write-Host "│ 🗑️ Removed : $Removed" -ForegroundColor White Write-Host "│ ⏭️ Skipped : $Skipped" -ForegroundColor White if ($Protected -gt 0) { Write-Host "│ 🛡️ Protected: $Protected" -ForegroundColor White } } else { # Default creation display Write-Host "│ ✅ Created : $Created" -ForegroundColor White Write-Host "│ ⏭️ Skipped : $Skipped" -ForegroundColor White Write-Host "│ ❌ Failed : $Failed" -ForegroundColor White } Write-Host "└───────────────────────────────────────────────────────────────────────────────┘" -ForegroundColor White } function Write-CleanupSummary { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] param( [Parameter(Mandatory = $true)] [string]$Category, [Parameter(Mandatory = $true)] [int]$Kept, [Parameter(Mandatory = $true)] [int]$Removed, [Parameter(Mandatory = $true)] [int]$Protected ) Write-Host "`n┌───────────────────────────────────────────────────────────────────────────────┐" Write-Host "│ SUMMARY: $Category" Write-Host "├───────────────────────────────────────────────────────────────────────────────┤" Write-Host "│ ✅ Kept : $Kept" Write-Host "│ 🗑️ Removed : $Removed" Write-Host "│ ⏭️ Protected: $Protected" Write-Host "└───────────────────────────────────────────────────────────────────────────────┘" if ($Protected -gt 0) { Write-Host "ℹ️ Protected assignments skipped: $Protected" } } function Write-CreationSummary { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] param( [Parameter(Mandatory = $true)] [string]$Category, [Parameter(Mandatory = $true)] [int]$Created, [Parameter(Mandatory = $true)] [int]$Skipped, [Parameter(Mandatory = $true)] [int]$Failed ) Write-Host "`n┌───────────────────────────────────────────────────────────────────────────────┐" Write-Host "│ SUMMARY: $Category" Write-Host "├───────────────────────────────────────────────────────────────────────────────┤" Write-Host "│ ✅ Created : $Created" Write-Host "│ ⏭️ Skipped : $Skipped" Write-Host "│ ❌ Failed : $Failed" Write-Host "└───────────────────────────────────────────────────────────────────────────────┘" } function New-EasyPIMAssignments { [CmdletBinding(SupportsShouldProcess=$true)] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true)] [PSCustomObject]$Config, [Parameter(Mandatory = $true)] [string]$TenantId, [Parameter()] [string]$SubscriptionId ) Write-SectionHeader "Processing Assignments" $results = [PSCustomObject]@{ Created = 0 Skipped = 0 Failed = 0 } # Process Azure Role eligible assignments if ($Config.AzureRoles -and $Config.AzureRoles.Count -gt 0) { Write-SubHeader "Processing Azure Role Eligible Assignments" $commandMap = New-CommandMap -ResourceType 'AzureRoleEligible' -TenantId $TenantId -SubscriptionId $SubscriptionId -FirstAssignment $Config.AzureRoles[0] $azureResult = Invoke-ResourceAssignments -ResourceType "Azure Role eligible" -Assignments $Config.AzureRoles -CommandMap $commandMap Write-Summary -Category "Azure Role Eligible Assignments" -Created $azureResult.Created -Skipped $azureResult.Skipped -Failed $azureResult.Failed $results.Created += $azureResult.Created $results.Skipped += $azureResult.Skipped $results.Failed += $azureResult.Failed } # Process Azure Role active assignments if ($Config.AzureRolesActive -and $Config.AzureRolesActive.Count -gt 0) { Write-SubHeader "Processing Azure Role Active Assignments" $commandMap = New-CommandMap -ResourceType 'AzureRoleActive' -TenantId $TenantId -SubscriptionId $SubscriptionId -FirstAssignment $Config.AzureRolesActive[0] $azureActiveResult = Invoke-ResourceAssignments -ResourceType "Azure Role active" -Assignments $Config.AzureRolesActive -CommandMap $commandMap Write-Summary -Category "Azure Role Active Assignments" -Created $azureActiveResult.Created -Skipped $azureActiveResult.Skipped -Failed $azureActiveResult.Failed $results.Created += $azureActiveResult.Created $results.Skipped += $azureActiveResult.Skipped $results.Failed += $azureActiveResult.Failed } # Process Entra ID Role eligible assignments if ($Config.EntraIDRoles -and $Config.EntraIDRoles.Count -gt 0) { Write-SubHeader "Processing Entra ID Role Eligible Assignments" $commandMap = New-CommandMap -ResourceType 'EntraRoleEligible' -TenantId $TenantId -FirstAssignment $Config.EntraIDRoles[0] $entraResult = Invoke-ResourceAssignments -ResourceType "Entra ID Role eligible" -Assignments $Config.EntraIDRoles -CommandMap $commandMap Write-Summary -Category "Entra ID Role Eligible Assignments" -Created $entraResult.Created -Skipped $entraResult.Skipped -Failed $entraResult.Failed $results.Created += $entraResult.Created $results.Skipped += $entraResult.Skipped $results.Failed += $entraResult.Failed } # Process Entra ID Role active assignments if ($Config.EntraIDRolesActive -and $Config.EntraIDRolesActive.Count -gt 0) { Write-SubHeader "Processing Entra ID Role Active Assignments" # Verify principals exist $validAssignments = $Config.EntraIDRolesActive | Where-Object { $exists = Test-PrincipalExists -PrincipalId $_.PrincipalId if (-not $exists) { Write-Warning "⚠️ Principal $($_.PrincipalId) does not exist, skipping assignment" $results.Failed++ return $false } return $true } if ($validAssignments.Count -gt 0) { $commandMap = New-CommandMap -ResourceType 'EntraRoleActive' -TenantId $TenantId -FirstAssignment $validAssignments[0] $entraActiveResult = Invoke-ResourceAssignments -ResourceType "Entra ID Role active" -Assignments $validAssignments -CommandMap $commandMap Write-Summary -Category "Entra ID Role Active Assignments" -Created $entraActiveResult.Created -Skipped $entraActiveResult.Skipped -Failed $entraActiveResult.Failed $results.Created += $entraActiveResult.Created $results.Skipped += $entraActiveResult.Skipped $results.Failed += $entraActiveResult.Failed } } # Process Group Role assignments with special handling if ($Config.GroupRoles -and $Config.GroupRoles.Count -gt 0) { # Group roles in separate sections by GroupId $groupsByGroupId = $Config.GroupRoles | Group-Object -Property GroupId foreach ($group in $groupsByGroupId) { $groupId = $group.Name $groupAssignments = $group.Group # Validate group exists and is eligible for PIM try { $uri = "https://graph.microsoft.com/v1.0/directoryObjects/$groupId" $null = Invoke-MgGraphRequest -Uri $uri -Method GET -ErrorAction Stop Write-Verbose "Group $groupId exists and is accessible" # Check if group is eligible for PIM if (-not (Test-GroupEligibleForPIM -GroupId $groupId)) { Write-Warning "⚠️ Group $groupId is not eligible for PIM management (likely synced from on-premises), skipping all assignments" $results.Skipped += $groupAssignments.Count continue # Skip this group entirely } } catch { Write-Warning "⚠️ Group $groupId does not exist, skipping all assignments for this group" $results.Failed += $groupAssignments.Count continue # Skip this group entirely } # Proceed with eligible group Write-SubHeader "Processing Group Role Eligible ($groupId) Assignments" # Pass groupId explicitly to New-CommandMap $commandMap = New-CommandMap -ResourceType 'GroupRoleEligible' -TenantId $TenantId -GroupId $groupId -FirstAssignment $groupAssignments[0] $groupResult = Invoke-ResourceAssignments -ResourceType "Group Role eligible ($groupId)" -Assignments $groupAssignments -CommandMap $commandMap # Add this summary call if it's missing Write-Summary -Category "Group Role Eligible Assignments ($groupId)" -Created $groupResult.Created -Skipped $groupResult.Skipped -Failed $groupResult.Failed # Update results $results.Created += $groupResult.Created $results.Skipped += $groupResult.Skipped $results.Failed += $groupResult.Failed } } # Process Group Role active assignments if ($Config.GroupRolesActive -and $Config.GroupRolesActive.Count -gt 0) { $groupRoleActiveResults = New-GroupRoleAssignments -Assignments $Config.GroupRolesActive -TenantId $TenantId -IsActive $true $results.Created += $groupRoleActiveResults.Created $results.Skipped += $groupRoleActiveResults.Skipped $results.Failed += $groupRoleActiveResults.Failed } return $results } function New-GroupRoleAssignments { [CmdletBinding(SupportsShouldProcess=$true)] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true)] [array]$Assignments, [Parameter(Mandatory = $true)] [string]$TenantId, [Parameter()] [bool]$IsActive = $false ) $roleType = if ($IsActive) { "Active" } else { "Eligible" } Write-SectionHeader "Processing Group Role $roleType Assignments" $results = [PSCustomObject]@{ Created = 0 Skipped = 0 Failed = 0 } # Group roles by GroupId to minimize API calls $groupedAssignments = $Assignments | Group-Object -Property GroupId foreach ($groupSet in $groupedAssignments) { $groupId = $groupSet.Name $assignmentsForGroup = $groupSet.Group Write-GroupHeader "Processing group: $groupId with $($assignmentsForGroup.Count) assignments" # First check if group exists before trying to process assignments if (-not (Test-PrincipalExists -PrincipalId $groupId)) { Write-StatusWarning "Group $groupId does not exist, skipping all assignments" $results.Failed += $assignmentsForGroup.Count continue } # Create the command map for this group $resourceType = if ($IsActive) { 'GroupRoleActive' } else { 'GroupRoleEligible' } $commandMap = New-CommandMap -ResourceType $resourceType -TenantId $TenantId -FirstAssignment $assignmentsForGroup[0] # Process assignments for this group $groupResult = Invoke-ResourceAssignments -ResourceType "Group Role $roleType ($groupId)" -Assignments $assignmentsForGroup -CommandMap $commandMap $results.Created += $groupResult.Created $results.Skipped += $groupResult.Skipped $results.Failed += $groupResult.Failed } # Display summary for this type of group assignments Write-Summary -Category "Group Role $roleType Assignments (Total)" -Created $results.Created -Skipped $results.Skipped -Failed $results.Failed return $results } function New-CommandMap { [CmdletBinding()] [OutputType([Hashtable])] param ( [Parameter(Mandatory = $true)] [ValidateSet('AzureRoleEligible', 'AzureRoleActive', 'EntraRoleEligible', 'EntraRoleActive', 'GroupRoleEligible', 'GroupRoleActive')] [string]$ResourceType, [Parameter(Mandatory = $true)] [string]$TenantId, [Parameter()] [string]$SubscriptionId, [Parameter()] [object]$FirstAssignment, [Parameter()] [string]$GroupId ) # Generate justification once $justification = Get-EasyPIMJustification -IncludeTimestamp Write-Verbose "Using justification: $justification" # Create the appropriate command map based on resource type switch ($ResourceType) { "AzureRoleEligible" { $commandMap = @{ GetCmd = 'Get-PIMAzureResourceEligibleAssignment' GetParams = @{ tenantID = $TenantId subscriptionID = $SubscriptionId } CreateCmd = 'New-PIMAzureResourceEligibleAssignment' CreateParams = @{ tenantID = $TenantId subscriptionID = $SubscriptionId justification = $justification # Explicitly set justification } DirectFilter = $true } } "AzureRoleActive" { $commandMap = @{ GetCmd = 'Get-PIMAzureResourceActiveAssignment' GetParams = @{ tenantID = $TenantId subscriptionID = $SubscriptionId } CreateCmd = 'New-PIMAzureResourceActiveAssignment' CreateParams = @{ tenantID = $TenantId subscriptionID = $SubscriptionId justification = $justification # Explicitly set justification } DirectFilter = $true } } "EntraRoleEligible" { $commandMap = @{ GetCmd = 'Get-PIMEntraRoleEligibleAssignment' GetParams = @{ tenantID = $TenantId } CreateCmd = 'New-PIMEntraRoleEligibleAssignment' CreateParams = @{ tenantID = $TenantId justification = $justification # Explicitly set justification } DirectFilter = $true } } "EntraRoleActive" { $commandMap = @{ GetCmd = 'Get-PIMEntraRoleActiveAssignment' GetParams = @{ tenantID = $TenantId } CreateCmd = 'New-PIMEntraRoleActiveAssignment' CreateParams = @{ tenantID = $TenantId justification = $justification # Explicitly set justification } DirectFilter = $true } } "GroupRoleEligible" { # Determine groupID - FirstAssignment.GroupId takes priority over parameter $effectiveGroupId = if ($FirstAssignment -and $FirstAssignment.GroupId) { $FirstAssignment.GroupId } elseif (-not [string]::IsNullOrEmpty($GroupId)) { $GroupId } else { Write-Warning "No GroupId available for GroupRoleEligible command map" $null } # IMPORTANT: Only include groupID in GetParams - it's required for listing $commandMap = @{ GetCmd = 'Get-PIMGroupEligibleAssignment' GetParams = @{ tenantID = $TenantId # Include groupID in GetParams ONLY - it's required groupID = $effectiveGroupId } CreateCmd = 'New-PIMGroupEligibleAssignment' CreateParams = @{ tenantID = $TenantId # DO NOT include other parameters like principalID, etc. # These will be added for each specific assignment justification = $justification } DirectFilter = $true } } "GroupRoleActive" { # Determine groupID - FirstAssignment.GroupId takes priority over parameter $effectiveGroupId = if ($FirstAssignment -and $FirstAssignment.GroupId) { $FirstAssignment.GroupId } elseif (-not [string]::IsNullOrEmpty($GroupId)) { $GroupId } else { Write-Warning "No GroupId available for GroupRoleActive command map" $null } # IMPORTANT: Only include groupID in GetParams - it's required for listing $commandMap = @{ GetCmd = 'Get-PIMGroupActiveAssignment' GetParams = @{ tenantID = $TenantId # Include groupID in GetParams ONLY - it's required groupID = $effectiveGroupId } CreateCmd = 'New-PIMGroupActiveAssignment' CreateParams = @{ tenantID = $TenantId # DO NOT include other parameters like principalID, etc. # These will be added for each specific assignment justification = $justification } DirectFilter = $true } } } return $commandMap } function Remove-JsonComments { [CmdletBinding()] [OutputType([System.String])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string]$JsonContent ) process { # Remove single-line comments (// ...) $JsonContent = [Regex]::Replace($JsonContent, '//.*?($|\r|\n)', '') # Remove multi-line comments (/* ... */) $JsonContent = [Regex]::Replace($JsonContent, '/\*[\s\S]*?\*/', '') return $JsonContent } } function Test-GroupEligibleForPIM { [CmdletBinding()] [OutputType([bool])] param ( [Parameter(Mandatory = $true)] [string]$GroupId ) try { # Get detailed group information with properties to check sync status $uri = "https://graph.microsoft.com/v1.0/groups/$GroupId`?`$select=id,displayName,onPremisesSyncEnabled,groupTypes" $groupDetails = Invoke-MgGraphRequest -Uri $uri -Method GET -ErrorAction Stop # Check if the group is synchronized from on-premises if ($groupDetails.onPremisesSyncEnabled -eq $true) { Write-Warning "Group $($groupDetails.displayName) ($GroupId) is synchronized from on-premises and cannot be managed by PIM" return $false } # Check if it's a Microsoft 365 group (which might have different PIM capabilities) if ($groupDetails.groupTypes -and $groupDetails.groupTypes -contains "Unified") { Write-Verbose "Group $($groupDetails.displayName) ($GroupId) is a Microsoft 365 group" # For now we'll consider these eligible, but you might need special handling } # Group is eligible for PIM return $true } catch { Write-Warning "Error checking group $GroupId eligibility for PIM: $_" # Default to false if we can't verify return $false } } function Write-EasyPIMSummary { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] param ( [Parameter()] [PSCustomObject]$CleanupResults, [Parameter()] [PSCustomObject]$AssignmentResults ) # Add grand total summary Write-Host "`n┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓" -ForegroundColor Green Write-Host "┃ OVERALL SUMMARY ┃" -ForegroundColor Green Write-Host "┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛" -ForegroundColor Green # Assignments section Write-Host "┌───────────────────────────────────────────────────────────────────────────────┐" -ForegroundColor White Write-Host "│ ASSIGNMENT CREATIONS" -ForegroundColor White Write-Host "├───────────────────────────────────────────────────────────────────────────────┤" -ForegroundColor White Write-Host "│ ✅ Created : $($AssignmentResults.Created)" -ForegroundColor White Write-Host "│ ⏭️ Skipped : $($AssignmentResults.Skipped)" -ForegroundColor White Write-Host "│ ❌ Failed : $($AssignmentResults.Failed)" -ForegroundColor White Write-Host "└───────────────────────────────────────────────────────────────────────────────┘" -ForegroundColor White # Cleanup section Write-Host "┌───────────────────────────────────────────────────────────────────────────────┐" -ForegroundColor White Write-Host "│ CLEANUP OPERATIONS" -ForegroundColor White Write-Host "├───────────────────────────────────────────────────────────────────────────────┤" -ForegroundColor White Write-Host "│ ✅ Kept : $($CleanupResults.Kept)" -ForegroundColor White Write-Host "│ 🗑️ Removed : $($CleanupResults.Removed)" -ForegroundColor White Write-Host "│ ⏭️ Skipped : $($CleanupResults.Skipped)" -ForegroundColor White if ($CleanupResults.Protected -gt 0) { Write-Host "│ 🛡️ Protected: $($CleanupResults.Protected)" -ForegroundColor White } Write-Host "└───────────────────────────────────────────────────────────────────────────────┘" -ForegroundColor White } <# .Synopsis Retrieve all role policies .Description Get all roles then for each get the policy .Parameter scope Scope to look at .Example PS> Get-AllPolicies -scope "subscriptions/$subscriptionID" Get all roles then for each get the policy .Link .Notes #> function Get-AllPolicies { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] param ( [Parameter()] [string] $scope ) $ARMhost = "https://management.azure.com" $ARMendpoint = "$ARMhost/$scope/providers/Microsoft.Authorization" $restUri = "$ARMendpoint/roleDefinitions?`$select=roleName&api-version=2022-04-01" write-verbose "Getting All Policies at $restUri" $response = Invoke-ARM -restURI $restUri -Method 'GET' -Body $null Write-Verbose $response $roles = $response | ForEach-Object { $_.value.properties.roleName } return $roles } <# .Synopsis Get rules for the role $rolename at the specified scope .Description will convert the json rules to a PSCustomObject .Parameter scope scope .Parameter rolename list of the role to check .Parameter copyfrom $true if this function is invoked for the Copy-PIMAzureReourcePolicy, we will parse the rules differently .Example PS> get-config -scope $scop -rolename role1 Get the policy of the role role1 at the specified scope .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function get-config ($scope, $rolename, $copyFrom = $null) { $ARMhost = "https://management.azure.com" $ARMendpoint = "$ARMhost/$scope/providers/Microsoft.Authorization" try { # 1 Get ID of the role $rolename assignable at the provided scope $restUri = "$ARMendpoint/roleDefinitions?api-version=2022-04-01&`$filter=roleName eq '$rolename'" write-verbose " #1 Get role definition for the role $rolename assignable at the scope $scope at $restUri" #$response = Invoke-RestMethod -Uri $restUri -Method Get -Headers $authHeader -verbose:$false $response = Invoke-ARM -restURI $restUri -method "get" -body $null $roleID = $response.value.id #if ($null -eq $roleID) { throw "An exception occured : can't find a roleID for $rolename at scope $scope" } Write-Verbose ">> RodeId = $roleID" if ( ($roleID -eq "") -or ($null -eq $roleID)) { Log "Error getting config of $rolename" #continue with other roles return } # 2 get the role assignment for the roleID found at #1 $restUri = "$ARMendpoint/roleManagementPolicyAssignments?api-version=2020-10-01&`$filter=roleDefinitionId eq '$roleID'" write-verbose " #2 Get the Assignment for $rolename at $restUri" #$response = Invoke-RestMethod -Uri $restUri -Method Get -Headers $authHeader -verbose:$false $response = Invoke-ARM -restURI $restUri -Method Get $policyId = $response.value.properties.policyId #.split('/')[-1] Write-Verbose ">> policy ID = $policyId" # 3 get the role policy for the policyID found in #2 $restUri = "$ARMhost/$policyId/?api-version=2020-10-01" write-verbose " #3 get role policy at $restUri" #$response = Invoke-RestMethod -Uri $restUri -Method Get -Headers $authHeader -verbose:$false $response = Invoke-ARM -restURI $restUri -Method Get #Write-Verbose "copy from = $copyFrom" if ($null -ne $copyFrom) { # Get access Token Write-Verbose ">> Getting access token" $token = Get-AzAccessToken # setting the authentication headers for MSGraph calls $authHeader = @{ 'Content-Type' = 'application/json' 'Authorization' = 'Bearer ' + $token.Token } Invoke-RestMethod -Uri $restUri -Method Get -Headers $authHeader -verbose:$false -OutFile "$_scriptPath\temp.json" $response = Get-Content "$_scriptPath\temp.json" $response = $response -replace '^.*"rules":\[' $response = $response -replace '\],"effectiveRules":.*$' Remove-Item "$_scriptPath\temp.json" return $response } #$response # Get config values in a new object: # Maximum end user activation duration in Hour (PT24H) // Max 24H in portal but can be greater $_activationDuration = $response.properties.rules | Where-Object { $_.id -eq "Expiration_EndUser_Assignment" } | Select-Object -ExpandProperty maximumduration # End user enablement rule (MultiFactorAuthentication, Justification, Ticketing) $_enablementRules = $response.properties.rules | Where-Object { $_.id -eq "Enablement_EndUser_Assignment" } | Select-Object -expand enabledRules # active assignment rules $_activeAssignmentRules = $response.properties.rules | Where-Object { $_.id -eq "Enablement_Admin_Assignment" } | Select-Object -expand enabledRules #Authentication Context Write-Verbose " >> Authentication Context response: \n $($response.properties.rules | Where-Object { $_.id -eq "AuthenticationContext_EndUser_Assignment" })" $_authenticationcontext_enabled = $response.properties.rules | Where-Object { $_.id -eq "AuthenticationContext_EndUser_Assignment" } | Select-Object -expand isEnabled if($false -eq $_authenticationcontext_enabled){ $_authenticationcontext_value = $null #fix issue #54 } else{ $_authenticationcontext_value = $response.properties.rules | Where-Object { $_.id -eq "AuthenticationContext_EndUser_Assignment" } |Select-Object -expand claimValue } # approval required $_approvalrequired = $($response.properties.rules | Where-Object { $_.id -eq "Approval_EndUser_Assignment" }).setting.isapprovalrequired # approvers $approvers = $($response.properties.rules | Where-Object { $_.id -eq "Approval_EndUser_Assignment" }).setting.approvalStages.primaryApprovers $approvers | ForEach-Object { $_approvers += '@{"id"="' + $_.id + '";"description"="' + $_.description + '";"userType"="' + $_.userType + '"},' } # permanent assignmnent eligibility $_eligibilityExpirationRequired = $response.properties.rules | Where-Object { $_.id -eq "Expiration_Admin_Eligibility" } | Select-Object -expand isExpirationRequired if ($_eligibilityExpirationRequired -eq "true") { $_permanantEligibility = "false" } else { $_permanantEligibility = "true" } # maximum assignment eligibility duration $_maxAssignmentDuration = $response.properties.rules | Where-Object { $_.id -eq "Expiration_Admin_Eligibility" } | Select-Object -expand maximumDuration # pemanent activation $_activeExpirationRequired = $response.properties.rules | Where-Object { $_.id -eq "Expiration_Admin_Assignment" } | Select-Object -expand isExpirationRequired if ($_activeExpirationRequired -eq "true") { $_permanantActiveAssignment = "false" } else { $_permanantActiveAssignment = "true" } # maximum activation duration $_maxActiveAssignmentDuration = $response.properties.rules | Where-Object { $_.id -eq "Expiration_Admin_Assignment" } | Select-Object -expand maximumDuration ################# # Notifications # ################# # Notification Eligibility Alert (Send notifications when members are assigned as eligible to this role) $_Notification_Admin_Admin_Eligibility = $response.properties.rules | Where-Object { $_.id -eq "Notification_Admin_Admin_Eligibility" } # Notification Eligibility Assignee (Send notifications when members are assigned as eligible to this role: Notification to the assigned user (assignee)) $_Notification_Eligibility_Assignee = $response.properties.rules | Where-Object { $_.id -eq "Notification_Requestor_Admin_Eligibility" } # Notification Eligibility Approvers (Send notifications when members are assigned as eligible to this role: request to approve a role assignment renewal/extension) $_Notification_Eligibility_Approvers = $response.properties.rules | Where-Object { $_.id -eq "Notification_Approver_Admin_Eligibility" } # Notification Active Assignment Alert (Send notifications when members are assigned as active to this role) $_Notification_Active_Alert = $response.properties.rules | Where-Object { $_.id -eq "Notification_Admin_Admin_Assignment" } # Notification Active Assignment Assignee (Send notifications when members are assigned as active to this role: Notification to the assigned user (assignee)) $_Notification_Active_Assignee = $response.properties.rules | Where-Object { $_.id -eq "Notification_Requestor_Admin_Assignment" } # Notification Active Assignment Approvers (Send notifications when members are assigned as active to this role: Request to approve a role assignment renewal/extension) $_Notification_Active_Approvers = $response.properties.rules | Where-Object { $_.id -eq "Notification_Approver_Admin_Assignment" } # Notification Role Activation Alert (Send notifications when eligible members activate this role: Role activation alert) $_Notification_Activation_Alert = $response.properties.rules | Where-Object { $_.id -eq "Notification_Admin_EndUser_Assignment" } # Notification Role Activation Assignee (Send notifications when eligible members activate this role: Notification to activated user (requestor)) $_Notification_Activation_Assignee = $response.properties.rules | Where-Object { $_.id -eq "Notification_Requestor_EndUser_Assignment" } # Notification Role Activation Approvers (Send notifications when eligible members activate this role: Request to approve an activation) $_Notification_Activation_Approver = $response.properties.rules | Where-Object { $_.id -eq "Notification_Approver_EndUser_Assignment" } $config = [PSCustomObject]@{ RoleName = $_ PolicyID = $policyId ActivationDuration = $_activationDuration EnablementRules = $_enablementRules -join ',' ActiveAssignmentRules = $_activeAssignmentRules -join ',' AuthenticationContext_Enabled = $_authenticationcontext_enabled AuthenticationContext_Value = $_authenticationcontext_value ApprovalRequired = $_approvalrequired Approvers = $_approvers -join ',' AllowPermanentEligibleAssignment = $_permanantEligibility MaximumEligibleAssignmentDuration = $_maxAssignmentDuration AllowPermanentActiveAssignment = $_permanantActiveAssignment MaximumActiveAssignmentDuration = $_maxActiveAssignmentDuration Notification_Eligibility_Alert_isDefaultRecipientEnabled = $($_Notification_Admin_Admin_Eligibility.isDefaultRecipientsEnabled) Notification_Eligibility_Alert_NotificationLevel = $($_Notification_Admin_Admin_Eligibility.notificationLevel) Notification_Eligibility_Alert_Recipients = $($_Notification_Admin_Admin_Eligibility.notificationRecipients) -join ',' Notification_Eligibility_Assignee_isDefaultRecipientEnabled = $($_Notification_Eligibility_Assignee.isDefaultRecipientsEnabled) Notification_Eligibility_Assignee_NotificationLevel = $($_Notification_Eligibility_Assignee.NotificationLevel) Notification_Eligibility_Assignee_Recipients = $($_Notification_Eligibility_Assignee.notificationRecipients) -join ',' Notification_Eligibility_Approvers_isDefaultRecipientEnabled = $($_Notification_Eligibility_Approvers.isDefaultRecipientsEnabled) Notification_Eligibility_Approvers_NotificationLevel = $($_Notification_Eligibility_Approvers.NotificationLevel) Notification_Eligibility_Approvers_Recipients = $($_Notification_Eligibility_Approvers.notificationRecipients -join ',') Notification_Active_Alert_isDefaultRecipientEnabled = $($_Notification_Active_Alert.isDefaultRecipientsEnabled) Notification_Active_Alert_NotificationLevel = $($_Notification_Active_Alert.notificationLevel) Notification_Active_Alert_Recipients = $($_Notification_Active_Alert.notificationRecipients -join ',') Notification_Active_Assignee_isDefaultRecipientEnabled = $($_Notification_Active_Assignee.isDefaultRecipientsEnabled) Notification_Active_Assignee_NotificationLevel = $($_Notification_Active_Assignee.notificationLevel) Notification_Active_Assignee_Recipients = $($_Notification_Active_Assignee.notificationRecipients -join ',') Notification_Active_Approvers_isDefaultRecipientEnabled = $($_Notification_Active_Approvers.isDefaultRecipientsEnabled) Notification_Active_Approvers_NotificationLevel = $($_Notification_Active_Approvers.notificationLevel) Notification_Active_Approvers_Recipients = $($_Notification_Active_Approvers.notificationRecipients -join ',') Notification_Activation_Alert_isDefaultRecipientEnabled = $($_Notification_Activation_Alert.isDefaultRecipientsEnabled) Notification_Activation_Alert_NotificationLevel = $($_Notification_Activation_Alert.NotificationLevel) Notification_Activation_Alert_Recipients = $($_Notification_Activation_Alert.NotificationRecipients -join ',') Notification_Activation_Assignee_isDefaultRecipientEnabled = $($_Notification_Activation_Assignee.isDefaultRecipientsEnabled) Notification_Activation_Assignee_NotificationLevel = $($_Notification_Activation_Assignee.NotificationLevel) Notification_Activation_Assignee_Recipients = $($_Notification_Activation_Assignee.NotificationRecipients -join ',') Notification_Activation_Approver_isDefaultRecipientEnabled = $($_Notification_Activation_Approver.isDefaultRecipientsEnabled) Notification_Activation_Approver_NotificationLevel = $($_Notification_Activation_Approver.NotificationLevel) Notification_Activation_Approver_Recipients = $($_Notification_Activation_Approver.NotificationRecipients -join ',') } return $config } catch { Mycatch $_ } } <# .Synopsis Retrieve all role .Description Get all roles then for each get the policy .Parameter tenantID Scope to look at .Example PS> Get-Entrarole -tenantID $tenantID Get all roles .Link .Notes #> function Get-Entrarole { [CmdletBinding()] param ( [Parameter()] [string] $tenantID ) $tenantID = $script:tenantID $endpoint="roleManagement/directory/roleDefinitions?`$select=displayname" write-verbose "Getting All Policies at $endpoint" $response = invoke-graph -Endpoint $endpoint $roles = $response | ForEach-Object { $_.value.displayname } return $roles } <# .Synopsis Get rules for the role $rolename .Description will convert the json rules to a PSCustomObject .Parameter rolename list of the role to check .Example PS> get-EntraRoleConfig -rolename "Global Administrator","Global Reader" Get the policy of the roles .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Get-EntraRoleConfig ($rolename) { try { # 1 Get roleID for $rolename $endpoint = "roleManagement/directory/roleDefinitions?`$filter=displayname eq '$rolename'" $response = invoke-graph -Endpoint $endpoint $roleID = $response.value.Id Write-Verbose "roleID = $roleID" if($null -eq $roleID){ Throw "ERROR: Role $rolename not found" return } # 2 Get PIM policyID for that role $endpoint = "policies/roleManagementPolicyAssignments?`$filter=scopeType eq 'DirectoryRole' and roleDefinitionId eq '$roleID' and scopeId eq '/' " Write-Verbose "endpoint = $endpoint" $response = invoke-graph -Endpoint $endpoint $policyID = $response.value.policyID Write-Verbose "policyID = $policyID" # 3 Get the rules $endpoint = "policies/roleManagementPolicies/$policyID/rules" $response = invoke-graph -Endpoint $endpoint #$response.value.properties #$response # Get config values in a new object: # Maximum end user activation duration in Hour (PT24H) // Max 24H in portal but can be greater $_activationDuration = $($response.value | Where-Object { $_.id -eq "Expiration_EndUser_Assignment" }).maximumDuration # | Select-Object -ExpandProperty maximumduration # End user enablement rule (MultiFactorAuthentication, Justification, Ticketing) $_enablementRules = $($response.value | Where-Object { $_.id -eq "Enablement_EndUser_Assignment" }).enabledRules # Active assignment requirement $_activeAssignmentRequirement = $($response.value | Where-Object { $_.id -eq "Enablement_Admin_Assignment" }).enabledRules # Authentication context $_authenticationContext_Enabled = $($response.value | Where-Object { $_.id -eq "AuthenticationContext_EndUser_Assignment" }).isEnabled $_authenticationContext_value = $($response.value | Where-Object { $_.id -eq "AuthenticationContext_EndUser_Assignment" }).claimValue # approval required $_approvalrequired = $($response.value | Where-Object { $_.id -eq "Approval_EndUser_Assignment" }).setting.isapprovalrequired # approvers $approvers = $($response.value | Where-Object { $_.id -eq "Approval_EndUser_Assignment" }).setting.approvalStages.primaryApprovers if(( $approvers | Measure-Object | Select-Object -ExpandProperty Count) -gt 0){ $approvers | ForEach-Object { if($_."@odata.type" -eq "#microsoft.graph.groupMembers"){ $_.userType = "group" $_.id=$_.groupID } else{ #"@odata.type": "#microsoft.graph.singleUser", $_.userType = "user" $_.id=$_.userID } $_approvers += '@{"id"="' + $_.id + '";"description"="' + $_.description + '";"userType"="' + $_.userType + '"},' } } # permanent assignmnent eligibility $_eligibilityExpirationRequired = $($response.value | Where-Object { $_.id -eq "Expiration_Admin_Eligibility" }).isExpirationRequired if ($_eligibilityExpirationRequired -eq "true") { $_permanantEligibility = "false" } else { $_permanantEligibility = "true" } # maximum assignment eligibility duration $_maxAssignmentDuration = $($response.value | Where-Object { $_.id -eq "Expiration_Admin_Eligibility" }).maximumDuration # pemanent activation $_activeExpirationRequired = $($response.value | Where-Object { $_.id -eq "Expiration_Admin_Assignment" }).isExpirationRequired if ($_activeExpirationRequired -eq "true") { $_permanantActiveAssignment = "false" } else { $_permanantActiveAssignment = "true" } # maximum activation duration $_maxActiveAssignmentDuration = $($response.value | Where-Object { $_.id -eq "Expiration_Admin_Assignment" }).maximumDuration ################# # Notifications # ################# # Notification Eligibility Alert (Send notifications when members are assigned as eligible to this role) $_Notification_Admin_Admin_Eligibility = $response.value | Where-Object { $_.id -eq "Notification_Admin_Admin_Eligibility" } # Notification Eligibility Assignee (Send notifications when members are assigned as eligible to this role: Notification to the assigned user (assignee)) $_Notification_Eligibility_Assignee = $response.value | Where-Object { $_.id -eq "Notification_Requestor_Admin_Eligibility" } # Notification Eligibility Approvers (Send notifications when members are assigned as eligible to this role: request to approve a role assignment renewal/extension) $_Notification_Eligibility_Approvers = $response.value | Where-Object { $_.id -eq "Notification_Approver_Admin_Eligibility" } # Notification Active Assignment Alert (Send notifications when members are assigned as active to this role) $_Notification_Active_Alert = $response.value | Where-Object { $_.id -eq "Notification_Admin_Admin_Assignment" } # Notification Active Assignment Assignee (Send notifications when members are assigned as active to this role: Notification to the assigned user (assignee)) $_Notification_Active_Assignee = $response.value | Where-Object { $_.id -eq "Notification_Requestor_Admin_Assignment" } # Notification Active Assignment Approvers (Send notifications when members are assigned as active to this role: Request to approve a role assignment renewal/extension) $_Notification_Active_Approvers = $response.value | Where-Object { $_.id -eq "Notification_Approver_Admin_Assignment" } # Notification Role Activation Alert (Send notifications when eligible members activate this role: Role activation alert) $_Notification_Activation_Alert = $response.value | Where-Object { $_.id -eq "Notification_Admin_EndUser_Assignment" } # Notification Role Activation Assignee (Send notifications when eligible members activate this role: Notification to activated user (requestor)) $_Notification_Activation_Assignee = $response.value | Where-Object { $_.id -eq "Notification_Requestor_EndUser_Assignment" } # Notification Role Activation Approvers (Send notifications when eligible members activate this role: Request to approve an activation) $_Notification_Activation_Approver = $response.value | Where-Object { $_.id -eq "Notification_Approver_EndUser_Assignment" } $config = [PSCustomObject]@{ RoleName = $_ roleID = $roleID PolicyID = $policyId ActivationDuration = $_activationDuration EnablementRules = $_enablementRules -join ',' ActiveAssignmentRequirement = $_activeAssignmentRequirement -join ',' AuthenticationContext_Enabled = $_authenticationContext_Enabled AuthenticationContext_Value = $_authenticationContext_value ApprovalRequired = $_approvalrequired Approvers = $_approvers -join ',' AllowPermanentEligibleAssignment = $_permanantEligibility MaximumEligibleAssignmentDuration = $_maxAssignmentDuration AllowPermanentActiveAssignment = $_permanantActiveAssignment MaximumActiveAssignmentDuration = $_maxActiveAssignmentDuration Notification_Eligibility_Alert_isDefaultRecipientEnabled = $($_Notification_Admin_Admin_Eligibility.isDefaultRecipientsEnabled) Notification_Eligibility_Alert_NotificationLevel = $($_Notification_Admin_Admin_Eligibility.notificationLevel) Notification_Eligibility_Alert_Recipients = $($_Notification_Admin_Admin_Eligibility.notificationRecipients) -join ',' Notification_Eligibility_Assignee_isDefaultRecipientEnabled = $($_Notification_Eligibility_Assignee.isDefaultRecipientsEnabled) Notification_Eligibility_Assignee_NotificationLevel = $($_Notification_Eligibility_Assignee.NotificationLevel) Notification_Eligibility_Assignee_Recipients = $($_Notification_Eligibility_Assignee.notificationRecipients) -join ',' Notification_Eligibility_Approvers_isDefaultRecipientEnabled = $($_Notification_Eligibility_Approvers.isDefaultRecipientsEnabled) Notification_Eligibility_Approvers_NotificationLevel = $($_Notification_Eligibility_Approvers.NotificationLevel) Notification_Eligibility_Approvers_Recipients = $($_Notification_Eligibility_Approvers.notificationRecipients -join ',') Notification_Active_Alert_isDefaultRecipientEnabled = $($_Notification_Active_Alert.isDefaultRecipientsEnabled) Notification_Active_Alert_NotificationLevel = $($_Notification_Active_Alert.notificationLevel) Notification_Active_Alert_Recipients = $($_Notification_Active_Alert.notificationRecipients -join ',') Notification_Active_Assignee_isDefaultRecipientEnabled = $($_Notification_Active_Assignee.isDefaultRecipientsEnabled) Notification_Active_Assignee_NotificationLevel = $($_Notification_Active_Assignee.notificationLevel) Notification_Active_Assignee_Recipients = $($_Notification_Active_Assignee.notificationRecipients -join ',') Notification_Active_Approvers_isDefaultRecipientEnabled = $($_Notification_Active_Approvers.isDefaultRecipientsEnabled) Notification_Active_Approvers_NotificationLevel = $($_Notification_Active_Approvers.notificationLevel) Notification_Active_Approvers_Recipients = $($_Notification_Active_Approvers.notificationRecipients -join ',') Notification_Activation_Alert_isDefaultRecipientEnabled = $($_Notification_Activation_Alert.isDefaultRecipientsEnabled) Notification_Activation_Alert_NotificationLevel = $($_Notification_Activation_Alert.NotificationLevel) Notification_Activation_Alert_Recipients = $($_Notification_Activation_Alert.NotificationRecipients -join ',') Notification_Activation_Assignee_isDefaultRecipientEnabled = $($_Notification_Activation_Assignee.isDefaultRecipientsEnabled) Notification_Activation_Assignee_NotificationLevel = $($_Notification_Activation_Assignee.NotificationLevel) Notification_Activation_Assignee_Recipients = $($_Notification_Activation_Assignee.NotificationRecipients -join ',') Notification_Activation_Approver_isDefaultRecipientEnabled = $($_Notification_Activation_Approver.isDefaultRecipientsEnabled) Notification_Activation_Approver_NotificationLevel = $($_Notification_Activation_Approver.NotificationLevel) Notification_Activation_Approver_Recipients = $($_Notification_Activation_Approver.NotificationRecipients -join ',') } return $config } catch { Mycatch $_ } } <# .Synopsis Get rules for the group $groupID .Description will convert the json rules to a PSCustomObject .Parameter id Id of the group to check .Parameter type type of role (owner or member) .Example PS> get-config -scope $scope -rolename role1 Get the policy of the role role1 at the specified scope .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function get-Groupconfig ( $id, $type) { try { $endpoint = "policies/roleManagementPolicyAssignments?`$filter=scopeId eq '$id' and scopeType eq 'Group' and roleDefinitionId eq '$type'&`$expand=policy(`$expand=rules)" $response = invoke-graph -Endpoint $endpoint $policyId=$response.value.policyid #$response # Get config values in a new object: # Maximum end user activation duration in Hour (PT24H) // Max 24H in portal but can be greater $_activationDuration = ($response.value.policy.rules | Where-Object { $_.id -eq "Expiration_EndUser_Assignment" }).maximumDuration # End user enablement rule (MultiFactorAuthentication, Justification, Ticketing) $_enablementRules = ($response.value.policy.rules | Where-Object { $_.id -eq "Enablement_EndUser_Assignment" }).enabledRules # Active assignment requirement $_activeAssignmentRequirement = ($response.value.policy.rules | Where-Object { $_.id -eq "Enablement_Admin_Assignment" }).enabledRules # Authentication context $_authenticationContext_Enabled = ($response.value.policy.rules | Where-Object { $_.id -eq "AuthenticationContext_EndUser_Assignment" }).isEnabled $_authenticationContext_value = ($response.value.policy.rules | Where-Object { $_.id -eq "AuthenticationContext_EndUser_Assignment" }).claimValue # approval required $_approvalrequired = $($response.value.policy.rules | Where-Object { $_.id -eq "Approval_EndUser_Assignment" }).setting.isapprovalrequired # approvers $approvers = $($response.value.policy.rules | Where-Object { $_.id -eq "Approval_EndUser_Assignment" }).setting.approvalStages.primaryApprovers if(( $approvers | Measure-Object | Select-Object -ExpandProperty Count) -gt 0){ $approvers | ForEach-Object { if($_."@odata.type" -eq "#microsoft.graph.groupMembers"){ $_.userType = "group" $_.id=$_.groupID } else{ #"@odata.type": "#microsoft.graph.singleUser", $_.userType = "user" $_.id=$_.userID } $_approvers += '@{"id"="' + $_.id + '";"description"="' + $_.description + '";"userType"="' + $_.userType + '"},' } } # permanent assignmnent eligibility $_eligibilityExpirationRequired = ($response.value.policy.rules | Where-Object { $_.id -eq "Expiration_Admin_Eligibility" }).isExpirationRequired if ($_eligibilityExpirationRequired -eq "true") { $_permanantEligibility = "false" } else { $_permanantEligibility = "true" } # maximum assignment eligibility duration $_maxAssignmentDuration = ($response.value.policy.rules | Where-Object { $_.id -eq "Expiration_Admin_Eligibility" }).maximumDuration # pemanent activation $_activeExpirationRequired = ($response.value.policy.rules | Where-Object { $_.id -eq "Expiration_Admin_Assignment" }).isExpirationRequired if ($_activeExpirationRequired -eq "true") { $_permanantActiveAssignment = "false" } else { $_permanantActiveAssignment = "true" } # maximum activation duration $_maxActiveAssignmentDuration = ($response.value.policy.rules | Where-Object { $_.id -eq "Expiration_Admin_Assignment" }).maximumDuration ################# # Notifications # ################# # Notification Eligibility Alert (Send notifications when members are assigned as eligible to this role) $_Notification_Admin_Admin_Eligibility = $response.value.policy.rules | Where-Object { $_.id -eq "Notification_Admin_Admin_Eligibility" } # Notification Eligibility Assignee (Send notifications when members are assigned as eligible to this role: Notification to the assigned user (assignee)) $_Notification_Eligibility_Assignee = $response.value.policy.rules | Where-Object { $_.id -eq "Notification_Requestor_Admin_Eligibility" } # Notification Eligibility Approvers (Send notifications when members are assigned as eligible to this role: request to approve a role assignment renewal/extension) $_Notification_Eligibility_Approvers = $response.value.policy.rules | Where-Object { $_.id -eq "Notification_Approver_Admin_Eligibility" } # Notification Active Assignment Alert (Send notifications when members are assigned as active to this role) $_Notification_Active_Alert = $response.value.policy.rules | Where-Object { $_.id -eq "Notification_Admin_Admin_Assignment" } # Notification Active Assignment Assignee (Send notifications when members are assigned as active to this role: Notification to the assigned user (assignee)) $_Notification_Active_Assignee = $response.value.policy.rules | Where-Object { $_.id -eq "Notification_Requestor_Admin_Assignment" } # Notification Active Assignment Approvers (Send notifications when members are assigned as active to this role: Request to approve a role assignment renewal/extension) $_Notification_Active_Approvers = $response.value.policy.rules | Where-Object { $_.id -eq "Notification_Approver_Admin_Assignment" } # Notification Role Activation Alert (Send notifications when eligible members activate this role: Role activation alert) $_Notification_Activation_Alert = $response.value.policy.rules | Where-Object { $_.id -eq "Notification_Admin_EndUser_Assignment" } # Notification Role Activation Assignee (Send notifications when eligible members activate this role: Notification to activated user (requestor)) $_Notification_Activation_Assignee = $response.value.policy.rules | Where-Object { $_.id -eq "Notification_Requestor_EndUser_Assignment" } # Notification Role Activation Approvers (Send notifications when eligible members activate this role: Request to approve an activation) $_Notification_Activation_Approver = $response.value.policy.rules | Where-Object { $_.id -eq "Notification_Approver_EndUser_Assignment" } $config = [PSCustomObject]@{ PolicyID = $policyId ActivationDuration = $_activationDuration EnablementRules = $_enablementRules -join ',' ActiveAssignmentRequirement = $_activeAssignmentRequirement -join ',' AuthenticationContext_Enabled = $_authenticationContext_Enabled AuthenticationContext_Value = $_authenticationContext_value ApprovalRequired = $_approvalrequired Approvers = $_approvers -join ',' AllowPermanentEligibleAssignment = $_permanantEligibility MaximumEligibleAssignmentDuration = $_maxAssignmentDuration AllowPermanentActiveAssignment = $_permanantActiveAssignment MaximumActiveAssignmentDuration = $_maxActiveAssignmentDuration Notification_Eligibility_Alert_isDefaultRecipientEnabled = $($_Notification_Admin_Admin_Eligibility.isDefaultRecipientsEnabled) Notification_Eligibility_Alert_NotificationLevel = $($_Notification_Admin_Admin_Eligibility.notificationLevel) Notification_Eligibility_Alert_Recipients = $($_Notification_Admin_Admin_Eligibility.notificationRecipients) -join ',' Notification_Eligibility_Assignee_isDefaultRecipientEnabled = $($_Notification_Eligibility_Assignee.isDefaultRecipientsEnabled) Notification_Eligibility_Assignee_NotificationLevel = $($_Notification_Eligibility_Assignee.NotificationLevel) Notification_Eligibility_Assignee_Recipients = $($_Notification_Eligibility_Assignee.notificationRecipients) -join ',' Notification_Eligibility_Approvers_isDefaultRecipientEnabled = $($_Notification_Eligibility_Approvers.isDefaultRecipientsEnabled) Notification_Eligibility_Approvers_NotificationLevel = $($_Notification_Eligibility_Approvers.NotificationLevel) Notification_Eligibility_Approvers_Recipients = $($_Notification_Eligibility_Approvers.notificationRecipients -join ',') Notification_Active_Alert_isDefaultRecipientEnabled = $($_Notification_Active_Alert.isDefaultRecipientsEnabled) Notification_Active_Alert_NotificationLevel = $($_Notification_Active_Alert.notificationLevel) Notification_Active_Alert_Recipients = $($_Notification_Active_Alert.notificationRecipients -join ',') Notification_Active_Assignee_isDefaultRecipientEnabled = $($_Notification_Active_Assignee.isDefaultRecipientsEnabled) Notification_Active_Assignee_NotificationLevel = $($_Notification_Active_Assignee.notificationLevel) Notification_Active_Assignee_Recipients = $($_Notification_Active_Assignee.notificationRecipients -join ',') Notification_Active_Approvers_isDefaultRecipientEnabled = $($_Notification_Active_Approvers.isDefaultRecipientsEnabled) Notification_Active_Approvers_NotificationLevel = $($_Notification_Active_Approvers.notificationLevel) Notification_Active_Approvers_Recipients = $($_Notification_Active_Approvers.notificationRecipients -join ',') Notification_Activation_Alert_isDefaultRecipientEnabled = $($_Notification_Activation_Alert.isDefaultRecipientsEnabled) Notification_Activation_Alert_NotificationLevel = $($_Notification_Activation_Alert.NotificationLevel) Notification_Activation_Alert_Recipients = $($_Notification_Activation_Alert.NotificationRecipients -join ',') Notification_Activation_Assignee_isDefaultRecipientEnabled = $($_Notification_Activation_Assignee.isDefaultRecipientsEnabled) Notification_Activation_Assignee_NotificationLevel = $($_Notification_Activation_Assignee.NotificationLevel) Notification_Activation_Assignee_Recipients = $($_Notification_Activation_Assignee.NotificationRecipients -join ',') Notification_Activation_Approver_isDefaultRecipientEnabled = $($_Notification_Activation_Approver.isDefaultRecipientsEnabled) Notification_Activation_Approver_NotificationLevel = $($_Notification_Activation_Approver.NotificationLevel) Notification_Activation_Approver_Recipients = $($_Notification_Activation_Approver.NotificationRecipients -join ',') } return $config } catch { Mycatch $_ } } # Cache for role mappings $script:roleCache = @{} function Get-RoleMappings { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param([string]$SubscriptionId) # Cache key for this subscription $cacheKey = "roles_$SubscriptionId" # Return cached result if available if ($script:roleCache.ContainsKey($cacheKey)) { return $script:roleCache[$cacheKey] } # Get roles and build mappings $roles = Get-AzRoleDefinition -Scope "/subscriptions/$SubscriptionId" $mapping = @{ NameToId = @{} IdToName = @{} FullPathToName = @{} } foreach ($role in $roles) { $mapping.NameToId[$role.Name] = $role.Id $mapping.IdToName[$role.Id] = $role.Name $fullPath = "/subscriptions/$SubscriptionId/providers/Microsoft.Authorization/roleDefinitions/$($role.Id)" $mapping.FullPathToName[$fullPath] = $role.Name } # Cache and return $script:roleCache[$cacheKey] = $mapping return $mapping } <# .Synopsis Import the settings from the csv file $path .Description Convert the csv back to policy rules .Parameter Path path to the csv file .Example PS> Import-EntraRoleSetting -path "c:\temp\myrole.csv" Import settings from file c:\temp\myrole.csv .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Import-EntraRoleSettings { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param( [Parameter(Mandatory = $true)] [String] $path ) log "Importing setting from $path" if (!(test-path $path)) { throw "Operation failed, file $path cannot be found" } $csv = Import-Csv $path $csv | ForEach-Object { $rules = @() $rules += Set-ActivationDuration $_.ActivationDuration -entrarole $enablementRules = $_.EnablementRules.Split(',') $rules += Set-ActivationRequirement $enablementRules -entraRole # $approvers = @() # $approvers += $_.approvers $rules += Set-ApprovalFromCSV $_.ApprovalRequired $_.Approvers -entraRole $rules += Set-EligibilityAssignmentFromCSV $_.MaximumEligibleAssignmentDuration $_.AllowPermanentEligibleAssignment -entraRole $rules += Set-ActiveAssignmentFromCSV $_.MaximumActiveAssignmentDuration $_.AllowPermanentActiveAssignment -entraRole $Notification_EligibleAssignment_Alert = @{ "isDefaultRecipientEnabled" = $_.Notification_Eligibility_Alert_isDefaultRecipientEnabled; "notificationLevel" = $_.Notification_Eligibility_Alert_notificationLevel; "Recipients" = $_.Notification_Eligibility_Alert_Recipients.split(',') } $rules += Set-Notification_EligibleAssignment_Alert $Notification_EligibleAssignment_Alert -EntraRole $Notification_EligibleAssignment_Assignee = @{ "isDefaultRecipientEnabled" = $_.Notification_Eligibility_Assignee_isDefaultRecipientEnabled; "notificationLevel" = $_.Notification_Eligibility_Assignee_notificationLevel; "Recipients" = $_.Notification_Eligibility_Assignee_Recipients.split(',') } $rules += Set-Notification_EligibleAssignment_Assignee $Notification_EligibleAssignment_Assignee -entraRole $Notification_EligibleAssignment_Approver = @{ "isDefaultRecipientEnabled" = $_.Notification_Eligibility_Approvers_isDefaultRecipientEnabled; "notificationLevel" = $_.Notification_Eligibility_Approvers_notificationLevel; "Recipients" = $_.Notification_Eligibility_Approvers_Recipients.split(',') } $rules += Set-Notification_EligibleAssignment_Approver $Notification_EligibleAssignment_Approver -entraRole $Notification_Active_Alert = @{ "isDefaultRecipientEnabled" = $_.Notification_Active_Alert_isDefaultRecipientEnabled; "notificationLevel" = $_.Notification_Active_Alert_notificationLevel; "Recipients" = $_.Notification_Active_Alert_Recipients.split(',') } $rules += Set-Notification_ActiveAssignment_Alert $Notification_Active_Alert -EntraRole $Notification_Active_Assignee = @{ "isDefaultRecipientEnabled" = $_.Notification_Active_Assignee_isDefaultRecipientEnabled; "notificationLevel" = $_.Notification_Active_Assignee_notificationLevel; "Recipients" = $_.Notification_Active_Assignee_Recipients.split(',') } $rules += Set-Notification_ActiveAssignment_Assignee $Notification_Active_Assignee -entraRole $Notification_Active_Approvers = @{ "isDefaultRecipientEnabled" = $_.Notification_Active_Approvers_isDefaultRecipientEnabled; "notificationLevel" = $_.Notification_Active_Approvers_notificationLevel; "Recipients" = $_.Notification_Active_Approvers_Recipients.split(',') } $rules += Set-Notification_ActiveAssignment_Approver $Notification_Active_Approvers -entraRole $Notification_Activation_Alert = @{ "isDefaultRecipientEnabled" = $_.Notification_Activation_Alert_isDefaultRecipientEnabled; "notificationLevel" = $_.Notification_Activation_Alert_notificationLevel; "Recipients" = $_.Notification_Activation_Alert_Recipients.split(',') } $rules += Set-Notification_Activation_Alert $Notification_Activation_Alert -entraRole $Notification_Activation_Assignee = @{ "isDefaultRecipientEnabled" = $_.Notification_Activation_Assignee_isDefaultRecipientEnabled; "notificationLevel" = $_.Notification_Activation_Assignee_notificationLevel; "Recipients" = $_.Notification_Activation_Assignee_Recipients.split(',') } $rules += Set-Notification_Activation_Assignee $Notification_Activation_Assignee -entraRole $Notification_Activation_Approver = @{ "isDefaultRecipientEnabled" = $_.Notification_Activation_Approver_isDefaultRecipientEnabled; "notificationLevel" = $_.Notification_Activation_Approver_notificationLevel; "Recipients" = $_.Notification_Activation_Approver_Recipients.split(',') } $rules += Set-Notification_Activation_Approver $Notification_Activation_Approver -entraRole <# #> # patch the policy Update-EntraRolePolicy $_.policyID $($rules -join ',') } } <# .Synopsis Import the settings from the csv file $path .Description Convert the csv back to policy rules .Parameter Path path to the csv file .Example PS> Import-Setting -path "c:\temp\myrole.csv" Import settings from file c:\temp\myrole.csv .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Import-Setting ($path) { log "Importing setting from $path" if (!(test-path $path)) { throw "Operation failed, file $path cannot be found" } $csv = Import-Csv $path $csv | ForEach-Object { $rules = @() $script:scope=$_.policyID -replace "/providers.*" $rules += Set-ActivationDuration $_.ActivationDuration $enablementRules = $_.EnablementRules.Split(',') $rules += Set-ActivationRequirement $enablementRules #$approvers = @() #$approvers += $_.approvers $rules += Set-ApprovalFromCSV $_.ApprovalRequired $_.Approvers $rules += Set-EligibilityAssignmentFromCSV $_.MaximumEligibleAssignmentDuration $_.AllowPermanentEligibleAssignment $rules += Set-ActiveAssignmentFromCSV $_.MaximumActiveAssignmentDuration $_.AllowPermanentActiveAssignment $Notification_EligibleAssignment_Alert = @{ "isDefaultRecipientEnabled" = $_.Notification_Eligibility_Alert_isDefaultRecipientEnabled; "notificationLevel" = $_.Notification_Eligibility_Alert_notificationLevel; "Recipients" = $_.Notification_Eligibility_Alert_Recipients.split(',') } $rules += Set-Notification_EligibleAssignment_Alert $Notification_EligibleAssignment_Alert $Notification_EligibleAssignment_Assignee = @{ "isDefaultRecipientEnabled" = $_.Notification_Eligibility_Assignee_isDefaultRecipientEnabled; "notificationLevel" = $_.Notification_Eligibility_Assignee_notificationLevel; "Recipients" = $_.Notification_Eligibility_Assignee_Recipients.split(',') } $rules += Set-Notification_EligibleAssignment_Assignee $Notification_EligibleAssignment_Assignee $Notification_EligibleAssignment_Approver = @{ "isDefaultRecipientEnabled" = $_.Notification_Eligibility_Approvers_isDefaultRecipientEnabled; "notificationLevel" = $_.Notification_Eligibility_Approvers_notificationLevel; "Recipients" = $_.Notification_Eligibility_Approvers_Recipients.split(',') } $rules += Set-Notification_EligibleAssignment_Approver $Notification_EligibleAssignment_Approver $Notification_Active_Alert = @{ "isDefaultRecipientEnabled" = $_.Notification_Active_Alert_isDefaultRecipientEnabled; "notificationLevel" = $_.Notification_Active_Alert_notificationLevel; "Recipients" = $_.Notification_Active_Alert_Recipients.split(',') } $rules += Set-Notification_ActiveAssignment_Alert $Notification_Active_Alert $Notification_Active_Assignee = @{ "isDefaultRecipientEnabled" = $_.Notification_Active_Assignee_isDefaultRecipientEnabled; "notificationLevel" = $_.Notification_Active_Assignee_notificationLevel; "Recipients" = $_.Notification_Active_Assignee_Recipients.split(',') } $rules += Set-Notification_ActiveAssignment_Assignee $Notification_Active_Assignee $Notification_Active_Approvers = @{ "isDefaultRecipientEnabled" = $_.Notification_Active_Approvers_isDefaultRecipientEnabled; "notificationLevel" = $_.Notification_Active_Approvers_notificationLevel; "Recipients" = $_.Notification_Active_Approvers_Recipients.split(',') } $rules += Set-Notification_ActiveAssignment_Approver $Notification_Active_Approvers $Notification_Activation_Alert = @{ "isDefaultRecipientEnabled" = $_.Notification_Activation_Alert_isDefaultRecipientEnabled; "notificationLevel" = $_.Notification_Activation_Alert_notificationLevel; "Recipients" = $_.Notification_Activation_Alert_Recipients.split(',') } $rules += Set-Notification_Activation_Alert $Notification_Activation_Alert $Notification_Activation_Assignee = @{ "isDefaultRecipientEnabled" = $_.Notification_Activation_Assignee_isDefaultRecipientEnabled; "notificationLevel" = $_.Notification_Activation_Assignee_notificationLevel; "Recipients" = $_.Notification_Activation_Assignee_Recipients.split(',') } $rules += Set-Notification_Activation_Assignee $Notification_Activation_Assignee $Notification_Activation_Approver = @{ "isDefaultRecipientEnabled" = $_.Notification_Activation_Approver_isDefaultRecipientEnabled; "notificationLevel" = $_.Notification_Activation_Approver_notificationLevel; "Recipients" = $_.Notification_Activation_Approver_Recipients.split(',') } $rules += Set-Notification_Activation_Approver $Notification_Activation_Approver #> # patch the policy Update-Policy $_.policyID $($rules -join ',') } } <# .Synopsis invoke ARM REST API .Description wrapper function to get an access token and set authentication header for each ARM API call .Parameter RestURI the URI .Parameter Method http method to use .Parameter Body an optional body .Example PS> invoke-ARM -restURI $restURI -method "GET" will send an GET query to $restURI and return the response .Link .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Invoke-ARM { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $restURI, [Parameter(Position = 1)] [System.String] $method, [Parameter(Position = 2)] [System.String] $body="" ) try{ <#$scope = "subscriptions/$script:subscriptionID" $ARMhost = "https://management.azure.com" $ARMendpoint = "$ARMhost/$scope/providers/Microsoft.Authorization"#> write-verbose "`n>> request body: $body" write-verbose "requested URI : $restURI ; method : $method" #TODO need better way to handle mangement group scope!! if($restURI -notmatch "managementgroups"){ $subscriptionMatches = [regex]::Matches($restURI,".*\/subscriptions\/([^\/]*).*") if ($subscriptionMatches.Count -gt 0 -and $subscriptionMatches.Groups.Count -gt 1) { $script:subscriptionID = $subscriptionMatches.Groups[1].Value } else { # If we can't extract it from the URI, try to use the one passed to the function if ($null -eq $script:subscriptionID -and $PSBoundParameters.ContainsKey('subscriptionID')) { $script:subscriptionID = $PSBoundParameters['subscriptionID'] } # Still null? Use the one from ApiInfo if ($null -eq $script:subscriptionID -and $ApiInfo -and $ApiInfo.Subscriptions -and $ApiInfo.Subscriptions.Count -gt 0) { $script:subscriptionID = $ApiInfo.Subscriptions[0] } # If we still don't have a subscription ID, we need to throw a better error if ($null -eq $script:subscriptionID) { throw "Could not determine subscription ID. Please provide it explicitly." } } if ( $null -eq (get-azcontext) -or ( (get-azcontext).Tenant.Id -ne $script:tenantID ) ) { Write-Verbose ">> Connecting to Azure with tenantID $script:tenantID" Connect-AzAccount -Tenantid $script:tenantID -Subscription $script:subscriptionID } } #replaced with invoke-azrestmethod <# # Get access Token Write-Verbose ">> Getting access token" # now this will return a securestring https://learn.microsoft.com/en-us/powershell/azure/upcoming-breaking-changes?view=azps-12.2.0#get-azaccesstoken $token = Get-AzAccessToken -AsSecureString # setting the authentication headers for MSGraph calls $authHeader = @{ 'Content-Type' = 'application/json' 'Authorization' = 'Bearer ' + $($token.Token | ConvertFrom-SecureString -AsPlainText) } if($body -ne ""){ $response = Invoke-RestMethod -Uri $restUri -Method $method -Headers $authHeader -Body $body -verbose:$false } else{ $response = Invoke-RestMethod -Uri $restUri -Method $method -Headers $authHeader -verbose:$false } #> if ($body -ne ""){ $response=Invoke-AZRestMethod -Method $method -Uri $restURI -payload $body } else { $response=Invoke-AZRestMethod -Method $method -Uri $restURI } return $response.content | convertfrom-json } catch{ MyCatch $_ } } <# .Synopsis invoke Microsoft Graph API .Description wrapper function to get an access token and set authentication header for each ARM API call .Parameter Endpoint the Graph endpoint .Parameter Method http method to use .Parameter Body an optional body .Example PS> invoke-Graph -URI $URI -method "GET" will send an GET query to $URI and return the response .Link .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function invoke-graph { [CmdletBinding()] param ( [Parameter()] [String] $Endpoint, [String] $Method = "GET", [String] $version = "v1.0", [String] $body ) try { $graph = "https://graph.microsoft.com/$version/" [string]$uri = $graph + $endpoint Write-Verbose "uri = $uri" if ( $null -eq (get-mgcontext) -or ( (get-mgcontext).TenantId -ne $script:tenantID ) ) { Write-Verbose ">> Connecting to Azure with tenantID $script:tenantID" $scopes = @( "RoleManagementPolicy.ReadWrite.Directory", "PrivilegedAccess.ReadWrite.AzureAD", "RoleManagement.ReadWrite.Directory", "RoleManagementPolicy.ReadWrite.AzureADGroup", "PrivilegedEligibilitySchedule.ReadWrite.AzureADGroup", "PrivilegedAssignmentSchedule.ReadWrite.AzureADGroup", "PrivilegedAccess.ReadWrite.AzureADGroup", "AuditLog.Read.All", "Directory.Read.All") Connect-MgGraph -Tenant $script:tenantID -Scopes $scopes -NoWelcome } if ( $body -ne "") { Invoke-MgGraphRequest -Uri "$uri" -Method $Method -Body $body -SkipHttpErrorCheck } else { Invoke-MgGraphRequest -Uri "$uri" -Method $Method -SkipHttpErrorCheck } } catch { MyCatch $_ } } <# .Synopsis Log message to file and display it on screen with basic colour hilighting. The function include a log rotate feature. .Description Write $msg to screen and file with additional inforamtions : date and time, name of the script from where the function was called, line number and user who ran the script. If logfile path isn't specified it will default to C:\UPF\LOGS\<scriptname.ps1.log> You can use $Maxsize and $MaxFile to specified the size and number of logfiles to keep (default is 3MB, and 3files) Use the switch $noEcho if you dont want the message be displayed on screen .Parameter msg The message to log .Parameter logfile Name of the logfile to use (default = <scriptname>.ps1.log) .Parameter logdir Path to the logfile's directory (defaut = <scriptpath>\LOGS) .Parameter noEcho Don't print message on screen .Parameter maxSize Maximum size (in bytes) before logfile is rotate (default is 3MB) .Parameter maxFile Number of logfile history to keep (default is 3) .EXAMPLE PS> log "A message to display on screen and file" message will be dispayed and saved to file .Example PS> log "this message will not appear on screen" -noEcho this message will be log to the file without any display .Link .Notes Changelog : * 27/08/2017 version initiale * 21/09/2017 correction of rotating step Todo : #> function log { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [CmdletBinding()] param( [string]$msg, $logfile = $null, $logdir = $(join-path -path $script:_logPath -childpath "LOGS"), # Path to logfile [switch] $noEcho, # if set dont display output to screen, only to logfile $MaxSize = 3145728, # 3MB #$MaxSize = 1, $Maxfile = 3 # how many files to keep ) #do nothing if logging is disabled if ($true -eq $script:logToFile ) { # When no logfile is specified we append .log to the scriptname if ( $null -eq $logfile ) { $logfile = "EasyPIM.log" } # Create folder if needed if ( !(test-path $logdir) ) { $null = New-Item -ItemType Directory -Path $logdir -Force } # Ensure logfile will be save in logdir if ( $logfile -notmatch [regex]::escape($logdir)) { $logfile = "$logdir\$logfile" } # Create file if ( !(Test-Path $logfile) ) { write-verbose "$logfile not found, creating it" $null = New-Item -ItemType file $logfile -Force } else { # file exists, do size exceeds limit ? if ( (get-childitem $logfile | Select-Object -expand length) -gt $Maxsize) { write-host "$(Get-Date -Format yyyy-MM-dd HH:mm) - $(whoami) - $($MyInvocation.ScriptName) (L $($MyInvocation.ScriptLineNumber)) : Log size exceed $MaxSize, creating a new file." >> $logfile # rename current logfile $LogFileName = $($($LogFile -split "\\")[-1]) $basename = Get-ChildItem $LogFile | Select-Object -expand basename $dirname = Get-ChildItem $LogFile | Select-Object -expand directoryname Write-Verbose "Rename-Item $LogFile ""$($LogFileName.substring(0,$LogFileName.length-4))-$(Get-Date -format yyyddMM-HHmmss).log""" Rename-Item $LogFile "$($LogFileName.substring(0,$LogFileName.length-4))-$(Get-Date -format yyyddMM-HHmmss).log" # keep $Maxfile logfiles and delete the older ones $filesToDelete = Get-ChildItem "$dirname\$basename*.log" | Sort-Object LastWriteTime -desc | Select-Object -Skip $Maxfile $filesToDelete | remove-item -force } } write-output "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") - $(whoami) - $($MyInvocation.ScriptName) (L $($MyInvocation.ScriptLineNumber)) : $msg" >> $logfile }# end logging to file # Display $msg if $noEcho is not set if ( $noEcho -eq $false) { #colour it up... if ( $msg -match "Erreur|error") { write-host $msg -ForegroundColor red } elseif ($msg -match "avertissement|attention|warning") { write-host $msg -ForegroundColor yellow } elseif ($msg -match "info|information") { write-host $msg -ForegroundColor cyan } elseif ($msg -match "succès|succes|success|OK") { write-host $msg -ForegroundColor green } else { write-host $msg } } } <# .Synopsis wrapper for all caught exceptions .Description the exception will be parsed to get the details, it will be logged and eventualy sent to Teams if the notification is enabled .Parameter e The exception that was sent .EXAMPLE PS> MyCatch $e Will log the details of the exception .Link .Notes #> function MyCatch($e){ write-verbose "MyCatch function called" $err = $($e.exception.message | out-string) $details =$e.errordetails# |fl -force $position = $e.InvocationInfo.positionMessage #$Exception = $e.Exception if ($TeamsNotif) { send-teamsnotif "$err" "$details<BR/> TIPS: try to check the scope and the role name" "$position" } Log "An exception occured: $err `nDetails: $details `nPosition: $position" throw "Error, script did not terminate gracefuly" #fix issue #40 } <# .Synopsis Send message to Teams channel .Description The app "inbound webhook" must be configured for that channed and the url set in scripts/variables.ps1 .Parameter message message to display .Parameter details placeholder for more details .Parameter myStackTrace place holder for stack trace .Example PS> send-teamsnotif "Error occured" "The source file was not found" Send a notification to teams webhook url .Notes #>function send-teamsnotif { [CmdletBinding()] #make script react as cmdlet (-verbose etc..) param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $message, [string] $details, [string] $myStackTrace = $null ) $JSONBody = @{ "@type" = "MessageCard" "@context" = "<http://schema.org/extensions>" "title" = "Alert for $description @ $env:computername " "text" = "An exception occured:" "sections" = @( @{ "activityTitle" = "Message : $message" }, @{ "activityTitle" = "Details : $details" }, @{ "activityTitle" = " Script path " "activityText" = "$_scriptFullName" }, @{ "activityTitle" = "myStackTrace" "activityText" = "$myStackTrace" } ) } $TeamMessageBody = ConvertTo-Json $JSONBody -Depth 100 $parameters = @{ "URI" = $teamsWebhookURL "Method" = 'POST' "Body" = $TeamMessageBody "ContentType" = 'application/json' } $null = Invoke-RestMethod @parameters } <# .Synopsis Rule for maximum activation duration .Description rule 1 in https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview#activation-rules .Parameter ActivationDuration Maximum activation duration. Duration ref: https://en.wikipedia.org/wiki/ISO_8601#Durations .PARAMETER entraRole Enable if we configure an Entra role .EXAMPLE PS> Set-ActivationDuration "PT8H" limit the activation duration to 8 hours .Link .Notes #> function Set-ActivationDuration ($ActivationDuration, [switch]$entraRole) { # Set Maximum activation duration if ( ($null -ne $ActivationDuration) -and ("" -ne $ActivationDuration) ) { Write-Verbose "Editing Activation duration : $ActivationDuration" $properties = @{ "isExpirationRequired" = "true"; "maximumDuration" = "$ActivationDuration"; "id" = "Expiration_EndUser_Assignment"; "ruleType" = "RoleManagementPolicyExpirationRule"; "target" = @{ "caller" = "EndUser"; "operations" = @("All") }; "level" = "Assignment" } $rule = $properties | ConvertTo-Json if ($entraRole) { $rule = ' { "@odata.type": "#microsoft.graph.unifiedRoleManagementPolicyExpirationRule", "id": "Expiration_EndUser_Assignment", "isExpirationRequired": "true", "maximumDuration": "'+ $ActivationDuration + '", "target": { "caller": "EndUser", "operations": [ "All" ], "level": "Assignment" } }' } #update rules if required return $rule } } <# .Synopsis Rule for activation requirement .Description rule 2 in https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview#activation-rules .Parameter ActivationRequirement value can be "None", or one or more value from "Justification","Ticketing","MultiFactoAuthentication" WARNING options are case sensitive! .EXAMPLE PS> Set-Activationrequirement "Justification" A justification will be required to activate the role .Link .Notes #> function Set-ActivationRequirement($ActivationRequirement, [switch]$entraRole) { write-verbose "Set-ActivationRequirement : $($ActivationRequirement.length)" if (($ActivationRequirement -eq "None") -or ($ActivationRequirement[0].length -eq 0 )) { #if none or a null array write-verbose "requirement is nul" $enabledRules = "[]," } else { write-verbose "requirement is NOT nul" $formatedRules = '[' $ActivationRequirement | ForEach-Object { $formatedRules += '"' $formatedRules += "$_" $formatedRules += '",' } #remove last comma $formatedRules = $formatedRules -replace “.$” $formatedRules += "]," $enabledRules = $formatedRules #Write-Verbose "************* $enabledRules " } $properties = '{ "enabledRules": '+ $enabledRules + ' "id": "Enablement_EndUser_Assignment", "ruleType": "RoleManagementPolicyEnablementRule", "target": { "caller": "EndUser", "operations": [ "All" ], "level": "Assignment", "targetObjects": [], "inheritableSettings": [], "enforcedSettings": [] } }' if ($entraRole) { $properties = ' { "@odata.type" : "#microsoft.graph.unifiedRoleManagementPolicyEnablementRule", "enabledRules": '+ $enabledRules + ' "id": "Enablement_EndUser_Assignment", "target": { "caller": "EndUser", "operations": [ "All" ], "level": "Assignment", "inheritableSettings": [], "enforcedSettings": [] } }' } return $properties } <# .Synopsis Rule for maximum active assignment .Description rule 6 in https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview#assignment-rules .Parameter MaximumActiveAssignmentDuration Maximum active assignment duration. Duration ref: https://en.wikipedia.org/wiki/ISO_8601#Durations .Parameter AllowPermanentActiveAssignment Allow permanent active assignement ? .Parameter EntraRole set to true if the rule is for an Entra role .EXAMPLE PS> Set-ActiveAssignment -MaximumActiveAssignmentDuration "P30D" -AllowPermanentActiveAssignment $false limit the active assignment duration to 30 days .Link .Notes #> function Set-ActiveAssignment($MaximumActiveAssignmentDuration, $AllowPermanentActiveAssignment, [switch]$EntraRole) { write-verbose "Set-ActiveAssignment($MaximumActiveAssignmentDuration, $AllowPermanentActiveAssignment)" if ( ($true -eq $AllowPermanentActiveAssignment) -or ("true" -eq $AllowPermanentActiveAssignment) -and ("false" -ne $AllowPermanentActiveAssignment)) { $expire2 = "false" } else { $expire2 = "true" } $rule = ' { "isExpirationRequired": '+ $expire2 + ', "maximumDuration": "'+ $MaximumActiveAssignmentDuration + '", "id": "Expiration_Admin_Assignment", "ruleType": "RoleManagementPolicyExpirationRule", "target": { "caller": "Admin", "operations": [ "All" ], "level": "Eligibility", "targetObjects": null, "inheritableSettings": null, "enforcedSettings": null } } ' if($EntraRole){ $rule = ' { "@odata.type": "#microsoft.graph.unifiedRoleManagementPolicyExpirationRule", "id": "Expiration_Admin_Assignment", "isExpirationRequired": '+ $expire2 + ', "maximumDuration": "'+ $MaximumActiveAssignmentDuration + '", "target": { "caller": "Admin", "operations": [ "all" ], "level": "Assignment", "inheritableSettings": [], "enforcedSettings": [] } }' } return $rule } <# .Synopsis Rule for maximum active assignment .Description rule 6 in https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview#assignment-rules .Parameter MaximumActiveAssignmentDuration Maximum active assignment duration. Duration ref: https://en.wikipedia.org/wiki/ISO_8601#Durations .Parameter AllowPermanentActiveAssignment Allow permanent active assignement ? .PARAMETER EntraRole set to true if configuration is for an entra role .EXAMPLE PS> Set-ActiveAssignment -MaximumActiveAssignmentDuration "P30D" -AllowPermanentActiveAssignment $false limit the active assignment duration to 30 days .Link .Notes #> function Set-ActiveAssignmentFromCSV($MaximumActiveAssignmentDuration, $AllowPermanentActiveAssignment, [switch]$EntraRole) { write-verbose "Set-ActiveAssignmentFromCSV($MaximumActiveAssignmentDuration, $AllowPermanentActiveAssignment)" if ( "true" -eq $AllowPermanentActiveAssignment) { $expire2 = "false" } else { $expire2 = "true" } $rule = ' { "isExpirationRequired": '+ $expire2 + ', "maximumDuration": "'+ $MaximumActiveAssignmentDuration + '", "id": "Expiration_Admin_Assignment", "ruleType": "RoleManagementPolicyExpirationRule", "target": { "caller": "Admin", "operations": [ "All" ], "level": "Eligibility", "targetObjects": null, "inheritableSettings": null, "enforcedSettings": null } } ' if($EntraRole){ $rule = ' { "@odata.type": "#microsoft.graph.unifiedRoleManagementPolicyExpirationRule", "id": "Expiration_Admin_Assignment", "isExpirationRequired": '+ $expire2 + ', "maximumDuration": "'+ $MaximumActiveAssignmentDuration + '", "target": { "caller": "Admin", "operations": [ "all" ], "level": "Assignment", "inheritableSettings": [], "enforcedSettings": [] } }' } return $rule } <# .Synopsis Rule for active assignment requirement .Description rule 2 in https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview#activation-rules .Parameter ActiveAssignmentRequirement value can be "None", or one or more value from "Justification","MultiFactoAuthentication" WARNING options are case sensitive! .EXAMPLE PS> Set-ActiveAssignmentRequirement "Justification" A justification will be required to activate the role .Link .Notes #> function Set-ActiveAssignmentRequirement($ActiveAssignmentRequirement, [switch]$entraRole) { write-verbose "Set-ActiveAssignmentRequirementt : $($ActiveAssignmentRequirement.length)" if (($ActiveAssignmentRequirement -eq "None") -or ($ActiveAssignmentRequirement[0].length -eq 0 )) { #if none or a null array write-verbose "requirement is null" $enabledRules = "[]," } else { write-verbose "requirement is NOT null" $formatedRules = '[' $ActiveAssignmentRequirement | ForEach-Object { $formatedRules += '"' $formatedRules += "$_" $formatedRules += '",' } #remove last comma $formatedRules = $formatedRules -replace “.$” $formatedRules += "]," $enabledRules = $formatedRules #Write-Verbose "************* $enabledRules " } $properties = '{ "enabledRules": '+ $enabledRules + ' "id": "Enablement_Admin_Assignment", "ruleType": "RoleManagementPolicyEnablementRule", "target": { "caller": "Admin", "operations": [ "All" ], "level": "Assignment", "targetObjects": [], "inheritableSettings": [], "enforcedSettings": [] } }' if ($entraRole) { $properties = ' { "@odata.type" : "#microsoft.graph.unifiedRoleManagementPolicyEnablementRule", "enabledRules": '+ $enabledRules + ' "id": "Enablement_Admin_Assignment", "target": { "caller": "EndUser", "operations": [ "All" ], "level": "Assignment", "inheritableSettings": [], "enforcedSettings": [] } }' } return $properties } <# .Synopsis Define if approval is required to activate a role, and who are the approvers .Description rule 4 in https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview#activation-rules .Parameter ApprovalRequired Do we need an approval to activate a role? .Parameter Approvers Who is the approver? .Parameter EntraRole Set to $true when editing an Entra Role .EXAMPLE PS> Set-Approval -ApprovalRequired $true -Approvers @(@{"Id"=$UID;"Name"="John":"Type"="user"}, @{"Id"=$GID;"Name"="Group1":"Type"="group"}) define John and Group1 as approvers and require approval .Link .Notes #> function Set-Approval ($ApprovalRequired, $Approvers, [switch]$entraRole) { try { Write-Verbose "Set-Approval started with ApprovalRequired=$ApprovalRequired and Approvers=$Approvers and entraRole=$entraRole" if ($null -eq $Approvers) { $Approvers = $script:config.Approvers } if ($ApprovalRequired -eq $false) { $req = "false" }else { $req = "true" } <#working sample {"properties":{"scope":"/subscriptions/eedcaa84-3756-4da9-bf87-40068c3dd2a2","rules":[ {"id":"Approval_EndUser_Assignment","ruleType":"RoleManagementPolicyApprovalRule", "target":{"caller":"EndUser","operations":["All"],"level":"Assignment"}, "setting":{"isApprovalRequired":false, "isApprovalRequiredForExtension":false, "isRequestorJustificationRequired":true, "approvalMode":"SingleStage", "approvalStages":[{"approvalStageTimeOutInDays":1,"isApproverJustificationRequired":true,"escalationTimeInMinutes":0,"isEscalationEnabled":false, "primaryApprovers":[{"id":"5dba24e0-00ef-4c21-9702-7c093a0775eb","userType":"Group","description":"0Ext_Partners","isBackup":false}, {"id":"00b34bb3-8a6b-45ce-a7bb-c7f7fb400507","userType":"User","description":"Bob MARLEY","isBackup":false}, {"id":"25f3deb5-1c8d-4035-942d-b3cbbad98b8e","userType":"User","description":"Loïc","isBackup":false}, {"id":"39014f60-8bf7-4d58-88e3-4d6f04f7c279","userType":"User","description":"Loic MICHEL","isBackup":false} ], "escalationApprovers":[] }]}}]} #> $rule = '{ "id":"Approval_EndUser_Assignment", "ruleType":"RoleManagementPolicyApprovalRule", "target":{ "caller":"EndUser", "operations":["All"], "level":"Assignment" }, "setting":{ "isApprovalRequired":"'+ $req + '", "isApprovalRequiredForExtension":false, "isRequestorJustificationRequired":true, "approvalMode":"SingleStage", "approvalStages":[{ "approvalStageTimeOutInDays":1, "isApproverJustificationRequired":true, "escalationTimeInMinutes":0, "isEscalationEnabled":false, "primaryApprovers":[ ' if ($PSBoundParameters.Keys.Contains('Approvers') -and ($null -ne $Approvers)) { $cpt = 0 $Approvers | ForEach-Object { #write-host $_ $id = $_.Id $name = $_.Name $type = ( Get-Culture ).TextInfo.ToTitleCase( $_.Type.ToLower() ) #capitalize first letter fix issue #30 if ($cpt -gt 0) { $rule += "," } $rule += ' { "id": "'+ $id + '", "description": "'+ $name + '", "isBackup": false, "userType": "'+ $type + '" } ' $cpt++ } $rule=$rule -replace ",$" #remove last comma } <#{"id":"5dba24e0-00ef-4c21-9702-7c093a0775eb","userType":"Group","description":"0Ext_Partners","isBackup":false}, {"id":"00b34bb3-8a6b-45ce-a7bb-c7f7fb400507","userType":"User","description":"Bob MARLEY","isBackup":false}, {"id":"25f3deb5-1c8d-4035-942d-b3cbbad98b8e","userType":"User","description":"Loïc","isBackup":false}, {"id":"39014f60-8bf7-4d58-88e3-4d6f04f7c279","userType":"User","description":"Loic MICHEL","isBackup":false}#> $rule += ' ], "escalationApprovers":[] }] } }' <# $rule = ' { "setting": {' if ($null -ne $ApprovalRequired) { $rule += '"isApprovalRequired": ' + $req + ',' } $rule += ' "isApprovalRequiredForExtension": false, "isRequestorJustificationRequired": true, "approvalMode": "SingleStage", "approvalStages": [ { "approvalStageTimeOutInDays": 1, "isApproverJustificationRequired": true, "escalationTimeInMinutes": 0, ' if ($null -ne $Approvers) { #at least one approver required if approval is enable $rule += ' "primaryApprovers": [ ' $cpt = 0 $Approvers | ForEach-Object { #write-host $_ $id = $_.Id $name = $_.Name $type = $_.Type if ($cpt -gt 0) { $rule += "," } $rule += ' { "id": "'+ $id + '", "description": "'+ $name + '", "isBackup": false, "userType": "'+ $type + '" } ' $cpt++ } $rule += ' ],' } $rule += ' "isEscalationEnabled": false, "escalationApprovers": null }] }, "id": "Approval_EndUser_Assignment", "ruleType": "RoleManagementPolicyApprovalRule", "target": { "caller": "EndUser", "operations": [ "All" ], "level": "Assignment", "targetObjects": null }, "inheritableSettings": null, "enforcedSettings": null }}' #> if ($entraRole) { $rule = ' { "@odata.type": "#microsoft.graph.unifiedRoleManagementPolicyApprovalRule", "id": "Approval_EndUser_Assignment", "target": { "caller": "EndUser", "operations": [ "all" ], "level": "Assignment", "inheritableSettings": [], "enforcedSettings": [] }, "setting": { "isApprovalRequired": ' $rule += $req $rule += ', "isApprovalRequiredForExtension": false, "isRequestorJustificationRequired": true, "approvalMode": "SingleStage", "approvalStages": [ { "approvalStageTimeOutInDays": 1, "isApproverJustificationRequired": true, "escalationTimeInMinutes": 0, "isEscalationEnabled": false, "primaryApprovers": [' if ($null -ne $Approvers) { #at least one approver required if approval is enable $cpt = 0 $Approvers | ForEach-Object { #write-host $_ $id = $_.Id $name = $_.Name ##$type = $_.Type if ($cpt -gt 0) { $rule += "," } $rule += ' { "@odata.type": "#microsoft.graph.singleUser", "isBackup": false, "id": "'+ $id + '", "description": "'+ $name + '", } ' $cpt++ } $rule += ' ], "escalationApprovers": [] } ] } }' } } return $rule } catch { MyCatch $_ } } <# .Synopsis Define if approval is required to activate a role, and who are the approvers .Description rule 4 in https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview#activation-rules .Parameter ApprovalRequired Do we need an approval to activate a role? .Parameter Approvers Who is the approver? .PARAMETER entrarole set to true if configuration is for an entra role .EXAMPLE PS> Set-Approval -ApprovalRequired $true -Approvers @(@{"Id"=$UID;"Name"="John":"Type"="user"}, @{"Id"=$GID;"Name"="Group1":"Type"="group"}) define John and Group1 as approvers and require approval .Link .Notes #> function Set-ApprovalFromCSV { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", "")] [CmdletBinding()] param( [Parameter(Mandatory=$false)] [string]$ApprovalRequired, [Parameter(Mandatory=$false)] [string]$Approvers, [Parameter(Mandatory=$false)] [switch]$entraRole ) write-verbose "Set-ApprovalFromCSV" if ($null -eq $Approvers) { $Approvers = $script:config.Approvers } if ($ApprovalRequired -eq "FALSE") { $req = "false" }else { $req = "true" } if (!$entraRole) { $rule = ' { "setting": {' if ($null -ne $ApprovalRequired) { $rule += '"isApprovalRequired":' + $req + ',' } $rule += ' "isApprovalRequiredForExtension": false, "isRequestorJustificationRequired": true, "approvalMode": "SingleStage", "approvalStages": [ { "approvalStageTimeOutInDays": 1, "isApproverJustificationRequired": true, "escalationTimeInMinutes": 0, ' if ($null -ne $Approvers) { #at least one approver required if approval is enable $Approvers = $Approvers -replace ",$" # remove the last comma # turn approvers list to an array $Approvers= $Approvers -replace "^","@(" $Approvers= $Approvers -replace "$",")" #write-verbose "APPROVERS: $Approvers" #then turn the sting into an array of hash table $Appr = Invoke-Expression $Approvers $rule += ' "primaryApprovers": [ ' $cpt = 0 $Appr| ForEach-Object { $id = $_.id $name = $_.description $type = $_.userType if ($cpt -gt 0) { $rule += "," } $rule += ' { "id": "'+ $id + '", "description": "'+ $name + '", "isBackup": false, "userType": "'+ $type + '" } ' $cpt++ } } $rule += ' ], "isEscalationEnabled": false, "escalationApprovers": null }] }, "id": "Approval_EndUser_Assignment", "ruleType": "RoleManagementPolicyApprovalRule", "target": { "caller": "EndUser", "operations": [ "All" ], "level": "Assignment", "targetObjects": null, "inheritableSettings": null, "enforcedSettings": null }}' } if ($entraRole) { $rule = ' { "@odata.type": "#microsoft.graph.unifiedRoleManagementPolicyApprovalRule", "id": "Approval_EndUser_Assignment", "target": { "caller": "EndUser", "operations": [ "all" ], "level": "Assignment", "inheritableSettings": [], "enforcedSettings": [] }, "setting": { "isApprovalRequired": ' $rule += $req $rule += ', "isApprovalRequiredForExtension": false, "isRequestorJustificationRequired": true, "approvalMode": "SingleStage", "approvalStages": [ { "approvalStageTimeOutInDays": 1, "isApproverJustificationRequired": true, "escalationTimeInMinutes": 0, "isEscalationEnabled": false, "primaryApprovers": [' if (($null -ne $Approvers) -and ("" -ne $Approvers)) { #at least one approver required if approval is enable $cpt = 0 # write-verbose "approvers: $approvers" $Approvers = $Approvers -replace ",$" # remove the last comma #then turn the sting into an array of hash table $list = Invoke-Expression $Approvers $list | ForEach-Object { $id = $_.id $name = $_.description #$type = $_.userType if ($cpt -gt 0) { $rule += "," } $rule += ' { "@odata.type": "#microsoft.graph.singleUser", "isBackup": false, "id": "'+ $id + '", "description": "'+ $name + '" } ' $cpt++ } } $rule += ' ], "escalationApprovers": [] } ] } }' } return $rule } <# .Synopsis Rule for authentication context .Description rule 3 in https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview#activation-rules .Parameter AuthenticationContext_Enabled $true or $false .PARAMETER AuthenticationContext_Value authentication context name ex "c1" .PARAMETER entraRole $true or $false .EXAMPLE PS> Set-AuthenticationContext -authenticationContext_Enabled $true -authenticationContext_Value "c1" Authentication context c1 will be required to activate the role .Link .Notes #> function Set-AuthenticationContext($authenticationContext_Enabled, $authenticationContext_Value, [switch]$entraRole) { write-verbose "Set-AuthenticationContext : $($authenticationContext_Enabled), $($authenticationContext_Value)" if ($true -eq $authenticationContext_Enabled) { $enabled = "true" if ($authenticationContext_Value -eq "None" -or $authenticationContext_Value.length -eq 0) { Throw "AuthenticationContext_Value cannot be null or empty if AuthenticationContext_Enabled is true" } if ( ([regex]::match($authenticationContext_Value, "c[0-9]{1,2}$").success -eq $false)) { Throw "AuthenticationContext_Value must be in the format c1 - c99" } } else { $enabled = "false" } $properties = '{ "id": "AuthenticationContext_EndUser_Assignment", "ruleType": "RoleManagementPolicyAuthenticationContextRule", "isEnabled": '+ $enabled + ', "claimValue": "'+ $authenticationContext_Value + '", "target": { "caller": "EndUser", "operations": [ "All" ], "level": "Assignment" } }' if ($entraRole) { $properties = ' { "@odata.type": "#microsoft.graph.unifiedRoleManagementPolicyAuthenticationContextRule", "id": "AuthenticationContext_EndUser_Assignment", "isEnabled": '+ $enabled + ', "claimValue": "'+ $authenticationContext_Value + '", "target": { "caller": "EndUser", "operations": [ "all" ], "level": "Assignment", "inheritableSettings": [], "enforcedSettings": [] } }' } return $properties } <# .Synopsis definne the eligible assignment setting : max duration and if permanent eligibility is allowed .Description correspond to rule 5 here: https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview#assignment-rules .Parameter MaximumEligibilityDuration maximum duration of an eligibility .Parameter AllowPermanentEligibility Do we allow permanent eligibility .Parameter EntraRole Set to $true if configuring entra role .Example PS> Set-EligibilityAssignment -MaximumEligibilityDuration "P30D" -AllowPermanentEligibility $false set Max eligibility duration to 30 days .Link .Notes #> function Set-EligibilityAssignment($MaximumEligibilityDuration, $AllowPermanentEligibility, [switch]$entraRole) { write-verbose "Set-EligibilityAssignment: $MaximumEligibilityDuration $AllowPermanentEligibility" $max = $MaximumEligibilityDuration if ( ($true -eq $AllowPermanentEligibility) -or ("true" -eq $AllowPermanentEligibility) -and ("false" -ne $AllowPermanentEligibility)) { $expire = "false" write-verbose "1 setting expire to : $expire" } else { $expire = "true" write-verbose "2 setting expire to : $expire" } $rule = ' { "isExpirationRequired": '+ $expire + ', "maximumDuration": "'+ $max + '", "id": "Expiration_Admin_Eligibility", "ruleType": "RoleManagementPolicyExpirationRule", "target": { "caller": "Admin", "operations": [ "All" ], "level": "Eligibility", "targetObjects": null, "inheritableSettings": null, "enforcedSettings": null } } ' if($entraRole){ $rule='{ "@odata.type": "#microsoft.graph.unifiedRoleManagementPolicyExpirationRule", "id": "Expiration_Admin_Eligibility", "isExpirationRequired": '+ $expire + ', "maximumDuration": "'+ $max + '", "target": { "caller": "Admin", "operations": [ "all" ], "level": "Eligibility", "inheritableSettings": [], "enforcedSettings": [] } }' } return $rule } <# .Synopsis definne the eligible assignment setting : max duration and if permanent eligibility is allowed .Description correspond to rule 5 here: https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview#assignment-rules .Parameter MaximumEligibilityDuration maximum duration of an eligibility .Parameter AllowPermanentEligibility Do we allow permanent eligibility .PARAMETER entraRole set to true if configuration is for an entra role .EXAMPLE PS> Set-EligibilityAssignment -MaximumEligibilityDuration "P30D" -AllowPermanentEligibility $false define a maximum eligibility duration of 30 days .Link .Notes #> function Set-EligibilityAssignmentFromCSV($MaximumEligibilityDuration, $AllowPermanentEligibility, [switch]$entraRole) { write-verbose "Set-EligibilityAssignmentFromCSV: $MaximumEligibilityDuration $AllowPermanentEligibility" $max = $MaximumEligibilityDuration if ( "true" -eq $AllowPermanentEligibility) { $expire = "false" write-verbose "1 setting expire to : $expire" } else { $expire = "true" write-verbose "2 setting expire to : $expire" } $rule = ' { "isExpirationRequired": '+ $expire + ', "maximumDuration": "'+ $max + '", "id": "Expiration_Admin_Eligibility", "ruleType": "RoleManagementPolicyExpirationRule", "target": { "caller": "Admin", "operations": [ "All" ], "level": "Eligibility", "targetObjects": null, "inheritableSettings": null, "enforcedSettings": null } } ' if($entraRole){ $rule='{ "@odata.type": "#microsoft.graph.unifiedRoleManagementPolicyExpirationRule", "id": "Expiration_Admin_Eligibility", "isExpirationRequired": '+ $expire + ', "maximumDuration": "'+ $max + '", "target": { "caller": "Admin", "operations": [ "all" ], "level": "Eligibility", "inheritableSettings": [], "enforcedSettings": [] } }' } # update rule only if a change was requested return $rule } <# .Synopsis Admin notification when a role is activated .Description notification setting corresponding to rule 15 here https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview#notification-rules .Parameter Notification_Activation_Alert hashtable for the settings like: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical";"Recipients" = @("email1@domain.com","email2@domain.com")} .PARAMETER entrarole set to true if configuration is for an entra role .Example PS> Set-Notification_Activation_Alert -Notification_Activation_Alert @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical";"Recipients" = @("email1@domain.com","email2@domain.com")} set the notification sent to Admins when a role is activated #> function Set-Notification_Activation_Alert($Notification_Activation_Alert, [switch]$entrarole) { $rule = ' { "notificationType": "Email", "recipientType": "Admin", "isDefaultRecipientsEnabled": '+ $Notification_Activation_Alert.isDefaultRecipientEnabled.ToLower() + ', "notificationLevel": "'+ $Notification_Activation_Alert.notificationLevel + '", "notificationRecipients": [ ' $Notification_Activation_Alert.Recipients | ForEach-Object { $rule += '"' + $_ + '",' } $rule = $rule -replace ".$" #remove the last comma $rule += ' ], "id": "Notification_Admin_EndUser_Assignment", "ruleType": "RoleManagementPolicyNotificationRule", "target": { "caller": "Admin", "operations": [ "All" ], "level": "Eligibility", "targetObjects": null, "inheritableSettings": null, "enforcedSettings": null } } ' if ($entrarole) { $rule='{ "@odata.type": "#microsoft.graph.unifiedRoleManagementPolicyNotificationRule", "id": "Notification_Admin_EndUser_Assignment", "notificationType": "Email", "recipientType": "Admin", "isDefaultRecipientsEnabled": '+ $Notification_Activation_Alert.isDefaultRecipientEnabled.ToLower() + ', "notificationLevel": "'+ $Notification_Activation_Alert.notificationLevel + '", "notificationRecipients": [' #write-verbose "recipient : $($Notification_ActiveAssignment_Assignee.Recipients)" If ( ($Notification_Activation_Alert.Recipients |Measure-Object |Select-Object -expand count) -gt 0 ){ $Notification_Activation_Alert.Recipients | ForEach-Object { $rule += '"' + $_ + '",' } $rule = $rule -replace ".$" #remove the last comma } $rule += '], "target": { "caller": "EndUser", "operations": [ "all" ], "level": "Assignment", "inheritableSettings": [], "enforcedSettings": [] } }' } return $rule } <# .Synopsis Approver notification when a role is activated .Description correspond to rule 1 here: https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview#notification-rules .Parameter Notification_Activation_Approver hashtable for the settings like: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical";"Recipients" = @("email1@domain.com","email2@domain.com")} .PARAMETER entrarole set to true if configuration is for an entra role .Example PS> Set-Notification_Activation_Alert -Notification_Activation_Alert @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical";"Recipients" = @("email1@domain.com","email2@domain.com")} set the notification sent to Admins when a role is activated .Link .Notes #> function Set-Notification_Activation_Approver ($Notification_Activation_Approver, [switch]$entrarole) { $rule = ' { "notificationType": "Email", "recipientType": "Approver", "isDefaultRecipientsEnabled": '+ $Notification_Activation_Approver.isDefaultRecipientEnabled.ToLower() + ', "notificationLevel": "'+ $Notification_Activation_Approver.notificationLevel + '", "notificationRecipients": [ ' <# # Cant add backup recipient for this rule $Notification_Activation_Approver.Recipients | % { $rule += '"' + $_ + '",' } #> $rule += ' ], "id": "Notification_Approver_EndUser_Assignment", "ruleType": "RoleManagementPolicyNotificationRule", "target": { "caller": "Admin", "operations": [ "All" ], "level": "Eligibility", "targetObjects": null, "inheritableSettings": null, "enforcedSettings": null } } ' if($entrarole){ #cant add additional recipients for this rule $rule='{ "@odata.type": "#microsoft.graph.unifiedRoleManagementPolicyNotificationRule", "id": "Notification_Approver_EndUser_Assignment", "notificationType": "Email", "recipientType": "Approver", "isDefaultRecipientsEnabled": '+ $Notification_Activation_Approver.isDefaultRecipientEnabled.ToLower() + ', "notificationLevel": "'+ $Notification_Activation_Approver.notificationLevel + '", "notificationRecipients": [], "target": { "caller": "EndUser", "operations": [ "all" ], "level": "Assignment", "inheritableSettings": [], "enforcedSettings": [] } } ' } return $rule } <# .Synopsis Assignee notification when a role is activated .Description correspond to rule 16 here: https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview#notification-rules .Parameter Notification_Activation_Assignee hashtable for the settings like: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical";"Recipients" = @("email1@domain.com","email2@domain.com")} .PARAMETER entrarole set to true if configuration is for an entra role .Example PS> Set-Notification_Activation_Assignee -Notification_Activation_Assignee @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical";"Recipients" = @("email1@domain.com","email2@domain.com")} set the notification sent to assignee when a role is activated .Link .Notes #> function set-Notification_Activation_Assignee($Notification_Activation_Assignee, [switch]$entrarole) { $rule = ' { "notificationType": "Email", "recipientType": "Requestor", "isDefaultRecipientsEnabled": '+ $Notification_Activation_Assignee.isDefaultRecipientEnabled.ToLower() + ', "notificationLevel": "'+ $Notification_Activation_Assignee.notificationLevel + '", "notificationRecipients": [ ' $Notification_Activation_Assignee.Recipients | ForEach-Object { $rule += '"' + $_ + '",' } $rule += ' ], "id": "Notification_Requestor_EndUser_Assignment", "ruleType": "RoleManagementPolicyNotificationRule", "target": { "caller": "Admin", "operations": [ "All" ], "level": "Eligibility", "targetObjects": null, "inheritableSettings": null, "enforcedSettings": null } } ' if ($entrarole) { $rule = ' { "@odata.type": "#microsoft.graph.unifiedRoleManagementPolicyNotificationRule", "id": "Notification_Requestor_EndUser_Assignment", "notificationType": "Email", "recipientType": "Requestor", "isDefaultRecipientsEnabled": '+ $Notification_Activation_Assignee.isDefaultRecipientEnabled.ToLower() + ', "notificationLevel": "'+ $Notification_Activation_Assignee.notificationLevel + '", "notificationRecipients": [' #write-verbose "recipient : $($Notification_ActiveAssignment_Assignee.Recipients)" If ( ($Notification_Activation_Assignee.Recipients |Measure-Object |Select-Object -expand count) -gt 0 ){ $Notification_Activation_Assignee.Recipients | ForEach-Object { $rule += '"' + $_ + '",' } $rule = $rule -replace ".$" } $rule += '], "target": { "caller": "EndUser", "operations": [ "all" ], "level": "Assignment", "inheritableSettings": [], "enforcedSettings": [] } } ' } return $rule } <# .Synopsis admin notification when an active assignment is created .Description correspond to rule 12 here: https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview#notification-rules .Parameter Notification_ActiveAssignment_Alert hashtable for the settings like: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical";"Recipients" = @("email1@domain.com","email2@domain.com")} .PARAMETER EntraRole set to true if the rule is for an Entra role .Example PS> Set-Notification_ActiveAssignment_Alert -Notification_ActiveAssignment_Alert @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical";"Recipients" = @("email1@domain.com","email2@domain.com")} set the notification sent to admin when active assignment is created .Link .Notes #> function Set-Notification_ActiveAssignment_Alert($Notification_ActiveAssignment_Alert, [switch]$EntraRole) { $rule = ' { "notificationType": "Email", "recipientType": "Admin", "isDefaultRecipientsEnabled": '+ $Notification_ActiveAssignment_Alert.isDefaultRecipientEnabled.ToLower() + ', "notificationLevel": "'+ $Notification_ActiveAssignment_Alert.notificationLevel + '", "notificationRecipients": [ ' $Notification_ActiveAssignment_Alert.Recipients | ForEach-Object { $rule += '"' + $_ + '",' } $rule = $rule -replace ",$" # remove the last comma $rule += ' ], "id": "Notification_Admin_Admin_Assignment", "ruleType": "RoleManagementPolicyNotificationRule", "target": { "caller": "Admin", "operations": [ "All" ], "level": "Eligibility", "targetObjects": null, "inheritableSettings": null, "enforcedSettings": null } } ' if($EntraRole){ $rule=' { "@odata.type": "#microsoft.graph.unifiedRoleManagementPolicyNotificationRule", "id": "Notification_Admin_Admin_Assignment", "notificationType": "Email", "recipientType": "Admin", "isDefaultRecipientsEnabled": '+ $Notification_ActiveAssignment_Alert.isDefaultRecipientEnabled.ToLower() + ', "notificationLevel": "'+ $Notification_ActiveAssignment_Alert.notificationLevel + '", "notificationRecipients": [ ' if( ($Notification_ActiveAssignment_Alert.Recipients |Measure-Object |Select-Object -expand count) -gt 0 ){ $Notification_ActiveAssignment_Alert.Recipients | ForEach-Object { $rule += '"' + $_ + '",' } $rule = $rule -replace ".$" } $rule += ' ], "target": { "caller": "Admin", "operations": [ "all" ], "level": "Assignment", "inheritableSettings": [], "enforcedSettings": [] } } ' } return $rule } <# .Synopsis approver notification when an active assignment is created .Description correspond to rule 14 here: https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview#notification-rules .Parameter Notification_ActiveAssignment_Approver hashtable for the settings like: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical";"Recipients" = @("email1@domain.com","email2@domain.com")} .PARAMETER entrarole set to true if configuration is for an entra role .Example PS> Set-Notification_ActiveAssignment_Approver -Notification_ActiveAssignment_Approver @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical";"Recipients" = @("email1@domain.com","email2@domain.com")} set the notification sent to approvers when active assignment is created .Link .Notes #> function Set-Notification_ActiveAssignment_Approver($Notification_ActiveAssignment_Approver, [switch]$entrarole) { $rule = ' { "notificationType": "Email", "recipientType": "Approver", "isDefaultRecipientsEnabled": '+ $Notification_ActiveAssignment_Approver.isDefaultRecipientEnabled.ToLower() + ', "notificationLevel": "'+ $Notification_ActiveAssignment_Approver.notificationLevel + '", "notificationRecipients": [ ' $Notification_ActiveAssignment_Approver.Recipients | ForEach-Object { $rule += '"' + $_ + '",' } $rule += ' ], "id": "Notification_Approver_Admin_Assignment", "ruleType": "RoleManagementPolicyNotificationRule", "target": { "caller": "Admin", "operations": [ "All" ], "level": "Eligibility", "targetObjects": null, "inheritableSettings": null, "enforcedSettings": null } } ' if( $entrarole){ $rule = '{ "@odata.type": "#microsoft.graph.unifiedRoleManagementPolicyNotificationRule", "id": "Notification_Approver_Admin_Assignment", "notificationType": "Email", "recipientType": "Approver", "isDefaultRecipientsEnabled": '+ $Notification_ActiveAssignment_Approver.isDefaultRecipientEnabled.ToLower() + ', "notificationLevel": "'+ $Notification_ActiveAssignment_Approver.notificationLevel + '", "notificationRecipients": [' #write-verbose "recipient : $($Notification_ActiveAssignment_Assignee.Recipients)" If ( ($Notification_ActiveAssignment_Approver.Recipients |Measure-Object |Select-Object -expand count) -gt 0 ){ $Notification_ActiveAssignment_Approver.Recipients | ForEach-Object { $rule += '"' + $_ + '",' } $rule = $rule -replace ".$" #remove the last comma } $rule += '], "target": { "caller": "Admin", "operations": [ "all" ], "level": "Assignment", "inheritableSettings": [], "enforcedSettings": [] } }' } return $rule } <# .Synopsis assignee notification when an active assignment is created .Description correspond to rule 13 here: https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview#notification-rules .Parameter Notification_ActiveAssignment_Alert hashtable for the settings like: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical";"Recipients" = @("email1@domain.com","email2@domain.com")} .PARAMETER entrarole set to true if configuration is for an entra role .Example PS> Set-Notification_ActiveAssignment_Assignee -Notification_ActiveAssignment_Assignee @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical";"Recipients" = @("email1@domain.com","email2@domain.com")} set the notification sent to assignee when active assignment is created .Link .Notes #>function Set-Notification_ActiveAssignment_Assignee($Notification_ActiveAssignment_Assignee, [switch]$entrarole) { $rule = ' { "notificationType": "Email", "recipientType": "Requestor", "isDefaultRecipientsEnabled": '+ $Notification_ActiveAssignment_Assignee.isDefaultRecipientEnabled.ToLower() + ', "notificationLevel": "'+ $Notification_ActiveAssignment_Assignee.notificationLevel + '", "notificationRecipients": [ ' $Notification_ActiveAssignment_Assignee.Recipients | ForEach-Object { $rule += '"' + $_ + '",' } $rule += ' ], "id": "Notification_Requestor_Admin_Assignment", "ruleType": "RoleManagementPolicyNotificationRule", "target": { "caller": "Admin", "operations": [ "All" ], "level": "Eligibility", "targetObjects": null, "inheritableSettings": null, "enforcedSettings": null } } ' if ($entrarole) { $rule = '{ "@odata.type": "#microsoft.graph.unifiedRoleManagementPolicyNotificationRule", "id": "Notification_Requestor_Admin_Assignment", "notificationType": "Email", "recipientType": "Requestor", "isDefaultRecipientsEnabled": '+ $Notification_ActiveAssignment_Assignee.isDefaultRecipientEnabled.ToLower() + ', "notificationLevel": "'+ $Notification_ActiveAssignment_Assignee.notificationLevel + '", "notificationRecipients": [' write-verbose "recipient : $($Notification_ActiveAssignment_Assignee.Recipients)" If ( ($Notification_ActiveAssignment_Assignee.Recipients |Measure-Object |Select-Object -expand count) -gt 0 ){ $Notification_ActiveAssignment_Assignee.Recipients | ForEach-Object { $rule += '"' + $_ + '",' } $rule = $rule -replace ".$" #remove the last comma } $rule += '], "target": { "caller": "Admin", "operations": [ "all" ], "level": "Assignment", "inheritableSettings": [], "enforcedSettings": [] } }' } return $rule } <# .Synopsis admin notification when an elligible assignment is created .Description correspond to rule 9 here: https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview#notification-rules .Parameter Notification_ActiveAssignment_Alert hashtable for the settings like: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical";"Recipients" = @("email1@domain.com","email2@domain.com")} .PARAMETER EntraRole set to true if the rule is for an Entra role .Example PS> Set-Notification_EligibleAssignment_Alert -Notification_EligibleAssignment_Alert @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical";"Recipients" = @("email1@domain.com","email2@domain.com")} set the notification sent to admin when elligible assignment is created .Link .Notes #> function Set-Notification_EligibleAssignment_Alert($Notification_EligibleAssignment_Alert, [switch]$EntraRole) { write-verbose "Set-Notification_EligibleAssignment_Alert($Notification_EligibleAssignment_Alert)" $rule = ' { "notificationType": "Email", "recipientType": "Admin", "isDefaultRecipientsEnabled": '+ $Notification_EligibleAssignment_Alert.isDefaultRecipientEnabled.ToLower() + ', "notificationLevel": "'+ $Notification_EligibleAssignment_Alert.notificationLevel + '", "notificationRecipients": [ ' $Notification_EligibleAssignment_Alert.Recipients | ForEach-Object { $rule += '"' + $_ + '",' } $rule = $rule -replace ".$" $rule += ' ], "id": "Notification_Admin_Admin_Eligibility", "ruleType": "RoleManagementPolicyNotificationRule", "target": { "caller": "Admin", "operations": [ "All" ], "level": "Eligibility", "targetObjects": null, "inheritableSettings": null, "enforcedSettings": null } } ' if($EntraRole){ $rule=' { "@odata.type": "#microsoft.graph.unifiedRoleManagementPolicyNotificationRule", "id": "Notification_Admin_Admin_Eligibility", "notificationType": "Email", "recipientType": "Admin", "isDefaultRecipientsEnabled": '+ $Notification_EligibleAssignment_Alert.isDefaultRecipientEnabled.ToLower() + ', "notificationLevel": "'+ $Notification_EligibleAssignment_Alert.notificationLevel + '", "notificationRecipients": [ ' $Notification_EligibleAssignment_Alert.Recipients | ForEach-Object { $rule += '"' + $_ + '",' } $rule = $rule -replace ".$" $rule += ' ], "target": { "caller": "Admin", "operations": [ "all" ], "level": "Eligibility", "inheritableSettings": [], "enforcedSettings": [] } } ' } write-verbose "end function notif elligible alert" return $rule } <# .Synopsis Approver notification when an elligible assignment is created .Description correspond to rule 11 here: https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview#notification-rules .Parameter Notification_EligibleAssignment_Approver hashtable for the settings like: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical";"Recipients" = @("email1@domain.com","email2@domain.com")} .PARAMETER EntraRole set to true if the rule is for an Entra role .Example PS> Set-Notification_EligibleAssignment_Approver -Notification_EligibleAssignment_Approver @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical";"Recipients" = @("email1@domain.com","email2@domain.com")} set the notification sent to approvers when elligible assignment is created .Link .Notes #> function Set-Notification_EligibleAssignment_Approver($Notification_EligibleAssignment_Approver, [switch]$EntraRole) { #write-verbose "function Set-Notification_EligibleAssignment_Approver" $rule = ' { "notificationType": "Email", "recipientType": "Approver", "isDefaultRecipientsEnabled": '+ $Notification_EligibleAssignment_Approver.isDefaultRecipientEnabled.ToLower() + ', "notificationLevel": "'+ $Notification_EligibleAssignment_Approver.notificationLevel + '", "notificationRecipients": [ ' $Notification_EligibleAssignment_Approver.recipients | ForEach-Object { $rule += '"' + $_ + '",' } $rule += ' ], "id": "Notification_Approver_Admin_Eligibility", "ruleType": "RoleManagementPolicyNotificationRule", "target": { "caller": "Admin", "operations": [ "All" ], "level": "Eligibility", "targetObjects": null, "inheritableSettings": null, "enforcedSettings": null } }' if($EntraRole){ $rule = ' { "@odata.type": "#microsoft.graph.unifiedRoleManagementPolicyNotificationRule", "id": "Notification_Approver_Admin_Eligibility", "notificationType": "Email", "recipientType": "Approver", "isDefaultRecipientsEnabled": '+ $Notification_EligibleAssignment_Approver.isDefaultRecipientEnabled.ToLower() + ', "notificationLevel": "'+ $Notification_EligibleAssignment_Approver.notificationLevel + '", "notificationRecipients": [' if( ( $Notification_EligibleAssignment_Approver.recipients |Measure-Object |Select-Object -ExpandProperty count) -gt 0){ $Notification_EligibleAssignment_Approver.recipients | ForEach-Object { $rule += '"' + $_ + '",' } $rule = $rule -replace ".$" } $rule += '], "target": { "caller": "Admin", "operations": [ "all" ], "level": "Eligibility", "inheritableSettings": [], "enforcedSettings": [] } } ' } return $rule } <# .Synopsis assignee notification when an elligible assignment is created .Description correspond to rule 10 here: https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview#notification-rules .Parameter Notification_EligibleAssignment_Assignee hashtable for the settings like: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical";"Recipients" = @("email1@domain.com","email2@domain.com")} .PARAMETER EntraRole set to true if the rule is for an Entra role .Example PS> Set-Notification_EligibleAssignment_Assignee -Notification_EligibleAssignment_Assignee @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical";"Recipients" = @("email1@domain.com","email2@domain.com")} set the notification sent to assignee when elligible assignment is created .Link .Notes #> function Set-Notification_EligibleAssignment_Assignee { [outputType([string])] [CmdletBinding(SupportsShouldProcess = $true)] param ( $Notification_EligibleAssignment_Assignee, [switch]$EntraRole ) $rule = ' { "notificationType": "Email", "recipientType": "Requestor", "isDefaultRecipientsEnabled": '+ $Notification_EligibleAssignment_Assignee.isDefaultRecipientEnabled.ToLower() + ', "notificationLevel": "'+ $Notification_EligibleAssignment_Assignee.notificationLevel + '", "notificationRecipients": [ ' $Notification_EligibleAssignment_Assignee.Recipients | ForEach-Object { $rule += '"' + $_ + '",' } $rule += ' ], "id": "Notification_Requestor_Admin_Eligibility", "ruleType": "RoleManagementPolicyNotificationRule", "target": { "caller": "Admin", "operations": [ "All" ], "level": "Eligibility", "targetObjects": null, "inheritableSettings": null, "enforcedSettings": null } }' if($EntraRole){ $rule = ' { "@odata.type": "#microsoft.graph.unifiedRoleManagementPolicyNotificationRule", "id": "Notification_Requestor_Admin_Eligibility", "notificationType": "Email", "recipientType": "Requestor", "isDefaultRecipientsEnabled": '+ $Notification_EligibleAssignment_Assignee.isDefaultRecipientEnabled.ToLower() + ', "notificationLevel": "'+ $Notification_EligibleAssignment_Assignee.notificationLevel + '", "notificationRecipients": [' If ( ($Notification_EligibleAssignment_Assignee.Recipients |Measure-Object |Select-Object -expand count) -gt 0 ){ $Notification_EligibleAssignment_Assignee.Recipients | ForEach-Object { $rule += '"' + $_ + '",' } $rule = $rule -replace ".$" #remove the last comma } $rule += '], "target": { "caller": "Admin", "operations": [ "all" ], "level": "Eligibility", "inheritableSettings": [], "enforcedSettings": [] }}' } return $rule } # Add caching for directory object lookups $script:principalCache = @{} function Test-PrincipalExists { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] param ([string]$PrincipalId) # Return from cache if available if ($script:principalCache.ContainsKey($PrincipalId)) { return $script:principalCache[$PrincipalId] } try { $response = Invoke-Graph -endpoint "directoryObjects/$PrincipalId" -ErrorAction SilentlyContinue if ($null -ne $response.error){ Write-Verbose "Principal $PrincipalId does not exist: $($response.error.message)" $script:principalCache[$PrincipalId] = $false return $false } else{ write-verbose "Principal $PrincipalId exists" $script:principalCache[$PrincipalId] = $true return $true } } catch { $script:principalCache[$PrincipalId] = $false return $false } } <# .Synopsis Update policy with new rules .Description Patch $policyID with the rules $rules .Parameter PolicyID policy ID .Parameter rules rules .Example PS> Update-Policy -policyID $id -rules $rules Update $policyID with rules $rules .Link .Notes #> function Update-EntraRolePolicy { [CmdletBinding(SupportsShouldProcess = $true)] param( $policyID, $rules ) Log "Updating Policy $policyID" -noEcho #write-verbose "rules: $rules" $endpoint="policies/roleManagementPolicies/$policyID" $body = ' { "rules": [ '+ $rules + '] }' write-verbose "`n>> PATCH body: $body" write-verbose "Patch endpoint : $endpoint" $response = invoke-graph -Endpoint $endpoint -Method "PATCH" -Body $body # return $response } <# .Synopsis Update policy with new rules .Description Patch $policyID with the rules $rules .Parameter PolicyID policy ID .Parameter rules rules .Example PS> Update-Policy -policyID $id -rules $rules Update $policyID with rules $rules .Link .Notes #> function Update-Policy { [CmdletBinding(SupportsShouldProcess = $true)] param( $policyID, $rules ) Write-Verbose "Updating Policy $policyID" write-Verbose "script:scope = $script:scope" #write-verbose "rules: $rules" $scope = $script:scope $ARMhost = "https://management.azure.com" #$ARMendpoint = "$ARMhost/$scope/providers/Microsoft.Authorization" $body = ' { "properties": { "scope": "'+ $scope + '", "rules": [ '+ $rules + '], "level": "Assignment" } }' $restUri = "$ARMhost/$PolicyId/?api-version=2020-10-01" <# write-verbose "`n>> PATCH body: $body" write-verbose "Patch URI : $restURI" $response = Invoke-RestMethod -Uri $restUri -Method PATCH -Headers $authHeader -Body $body -verbose:$false #> $response = invoke-ARM -restURI $restUri -Method "PATCH" -Body $body # return $response } <# .Synopsis EASYPIM Powershell module to manage PIM Azure Resource Role settings with simplicity in mind Get-PIMEntraRolePolicy will return the policy rules (like require MFA on activation) of the selected rolename at the subscription level Support querrying multi roles at once .Description Get-PIMAzureResourcePendingApproval will use the Microsoft Graph APIs to retrieve the requests pending your approval .PARAMETER approvalID approval ID from get-PIMAzureResourcePendingApproval .PARAMETER justification justification for the approval .Example PS> approve-PIMAzureResourcePendingApproval -approvalID $approvalID -justification "I approve this request" Approve a pending request .Link .Notes Homepage: https://github.com/kayasax/easyPIM Author: MICHEL, Loic Changelog: Todo: * allow other scopes #> function Approve-PIMAzureResourcePendingApproval { [CmdletBinding()] [OutputType([String])] param ( [Parameter(Position = 0, Mandatory = $true,ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [System.String] # Tenant ID $approvalID, [Parameter(Position = 1, Mandatory = $true)] [System.String] # justification $justification ) process{ try { $script:tenantID = $tenantID Write-Verbose "approve-PIMAzureResourcePendingApproval start with parameters: approvalid => $approvalID, justification => $justification" #Get the stages: #Role Assignment Approval Steps - List - REST API (Azure Authorization) | Microsoft Learn $stages=Invoke-AzRestMethod -Uri "https://management.azure.com/$approvalID/stages?api-version=2021-01-01-preview" -Method GET $stageid=($stages.Content | convertfrom-json).value.id #approve the request #Role Assignment Approval Step - Patch - REST API (Azure Authorization) | Microsoft Learn $body='{"properties":{"justification":"'+$justification+'","reviewResult":"Approve"}}' Invoke-AzRestMethod -Uri "https://management.azure.com/$stageid/?api-version=2021-01-01-preview" -Payload $body -Method PUT return "Success, request approved" } catch { MyCatch $_ } } } <# .Synopsis EASYPIM Powershell module to manage PIM Azure Resource Role settings with simplicity in mind Get-PIMEntraRolePolicy will return the policy rules (like require MFA on activation) of the selected rolename at the subscription level Support querrying multi roles at once .Description Approve-PIMEntraRolePendingApprovall will use the Microsoft Graph APIs to retrieve the requests pending your approval .PARAMETER approvalID approval ID from get-PIMAzureResourcePendingApproval .PARAMETER justification justification for the approval .Example PS> approve-PIMAzureResourcePendingApproval -approvalID $approvalID -justification "I approve this request" Approve a pending request .Link .Notes Homepage: https://github.com/kayasax/easyPIM Author: MICHEL, Loic Changelog: Todo: * allow other scopes #> function Approve-PIMEntraRolePendingApproval { [CmdletBinding()] [OutputType([String])] param ( [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [System.String] # Approval ID $approvalID, [Parameter(Position = 1, Mandatory = $true)] [System.String] # justification $justification ) process { try { #$script:tenantID = $tenantID Write-Verbose "approve-PIMEntraRolePendingApproval start with parameters: approvalid => $approvalID, justification => $justification" #Get the stages: #Role Assignment Approval Steps - List - REST API (Azure Authorization) | Microsoft Learn $stages = Invoke-graph -endpoint "roleManagement/directory/roleAssignmentApprovals/$approvalID/" -Method GET -version "beta" $stageid = $stages.id #approve the request #Role Assignment Approval Step - Patch - REST API (Azure Authorization) | Microsoft Learn $body = '{"justification":"' + $justification + '","reviewResult":"Approve"}' Invoke-graph -endpoint "roleManagement/directory/roleAssignmentApprovals/$approvalID/steps/$stageID" -body $body -version "beta" -Method PATCH return "Success, request approved" } catch { MyCatch $_ } } } <# .Synopsis EASYPIM Powershell module to manage PIM Azure Resource Role settings with simplicity in mind Get-PIMGroupPolicy will return the policy rules (like require MFA on activation) of the selected rolename at the subscription level Support querrying multi roles at once .Description Approve-PIMGroupPendingApprovall will use the Microsoft Graph APIs to retrieve the requests pending your approval .PARAMETER approvalID approval ID from get-PIMAzureResourcePendingApproval .PARAMETER justification justification for the approval .Example PS> approve-PIMAzureResourcePendingApproval -approvalID $approvalID -justification "I approve this request" Approve a pending request .Link .Notes Homepage: https://github.com/kayasax/easyPIM Author: MICHEL, Loic Changelog: Todo: * allow other scopes #> function Approve-PIMGroupPendingApproval { [CmdletBinding()] [OutputType([String])] param ( [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [System.String] # Approval ID $approvalID, [Parameter(Position = 1, Mandatory = $true)] [System.String] # justification $justification ) process { try { #$script:tenantID = $tenantID Write-Verbose "approve-PIMGroupPendingApproval start with parameters: approvalid => $approvalID, justification => $justification" #Get the stages: #in groups stageID is the same as the approvalID #approve the request #https://learn.microsoft.com/en-us/graph/api/approvalstage-update?view=graph-rest-1.0&tabs=http $body = '{"justification":"' + $justification + '","reviewResult":"Approve"}' Invoke-graph -endpoint "identityGovernance/privilegedAccess/group/assignmentApprovals/$approvalID/steps/$approvalID" -body $body -version "beta" -Method PATCH return "Success, request approved" } catch { MyCatch $_ } } } <# .Synopsis Export PIM settings of all roles at the subscription scope to a csv file. Use the exportFilename parameter to specify the csv file, if not specified default filename will be %appdata%\powershell\EasyPIM\Exports\backup_<date>.csv .Description Convert the policy rules to a csv file .Example PS> Export-PIMAzureResourcePolicy -tennantID $tenantID -subscriptionID $subscriptionID -filename "c:\temp\myrole.csv" Export settings of all roles to file c:\temp\myrole.csv .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Backup-PIMAzureResourcePolicy { [CmdletBinding(DefaultParameterSetName='Default')] param ( [Parameter(Position = 0, Mandatory = $true)] [System.String] # Tenant ID $tenantID, [Parameter(ParameterSetName = 'Default',Position = 1, Mandatory = $true)] [System.String] # subscription id $subscriptionID, [Parameter(ParameterSetName = 'Scope',Position = 1, Mandatory = $true)] [System.String] # scope $scope, [Parameter(Position = 2)] [System.String] # Filename of the csv to generate $exportFilename ) try { $script:tenantID = $tenantID $exports = @() if (!($PSBoundParameters.Keys.Contains('scope'))) { $scope = "subscriptions/$subscriptionID" } $policies = Get-AllPolicies $scope $policies | ForEach-Object { log "exporting $_ role settings" #write-verbose $_ $exports += get-config $scope $_.Trim() } $date = get-date -Format FileDateTime if (!($exportFilename)) { $exportFilename = "$script:_LogPath\EXPORTS\BACKUP_$date.csv" } log "exporting to $exportFilename" $exportPath = Split-Path $exportFilename -Parent #create export folder if no exist if ( !(test-path $exportPath) ) { $null = New-Item -ItemType Directory -Path $exportPath -Force } $exports | Select-Object * | ConvertTo-Csv | out-file $exportFilename } catch { MyCatch $_ } } <# .Synopsis Export PIM settings of all roles to a csv file. Use the path parameter to specify the csv file, if not specified default filename will be %appdata%\powershell\EasyPIM\Exports\BACKUP_EntraRole_<date>.csv .Description Convert the policy rules to a csv file .Example PS> Export-PIMEntraRolePolicy -tennantID $tenantID -path "c:\temp\myrole.csv" Export settings of all roles to file c:\temp\myrole.csv .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Backup-PIMEntraRolePolicy { [CmdletBinding(DefaultParameterSetName='Default')] param ( [Parameter(Position = 0, Mandatory = $true)] [System.String] # Tenant ID $tenantID, [Parameter(Position = 2)] [System.String] # Filename of the csv to generate $path ) try { $script:tenantID = $tenantID $exports = @() $roles=get-entraRole $roles | ForEach-Object { log "exporting $_ role settings" #write-verbose $_ $exports += get-EntraRoleconfig $_.Trim() } $date = get-date -Format FileDateTime if (!($path)) { $path = "$script:_LogPath\EXPORTS\BACKUP_EntraRole_$date.csv" } log "exporting to $path" $exportPath = Split-Path $path -Parent #create export folder if no exist if ( !(test-path $path) ) { $null = New-Item -ItemType Directory -Path $exportPath -Force } $exports | Select-Object * | ConvertTo-Csv | out-file $path } catch { MyCatch $_ } } <# .Synopsis Copy eligible assignement from one user to another .Description https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Parameter tenantID EntraID tenant ID .Parameter subscriptionID subscription ID .Parameter scope use scope parameter if you want to work at other scope than a subscription .PARAMETER from userprincipalname or objectID of the source object .Parameter to userprincipalname or objectID of the destination object .Example PS> Copy-PIMAzureResourceEligibleAssignment -tenantID $tid -subscriptionID -subscription $subscription -from user1@contoso.com -to user2@contoso.com Copy eligible assignement from user1 to user2 .Link .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Copy-PIMAzureResourceEligibleAssignment { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [String] $tenantID, [Parameter(Position = 1)] [String] $subscriptionID, [Parameter()] [String] $scope, [Parameter(Mandatory = $true)] [String] $from, [Parameter(Mandatory = $true)] [String] $to ) try { $script:tenantID = $tenantID if (!($PSBoundParameters.Keys.Contains('scope'))) { $scope = "/subscriptions/$subscriptionID" } #convert UPN to objectID if ($from -match ".+@.*\..+") { #if this is a upn we will use graph to get the objectID try { $resu = invoke-graph -endpoint "users/$from" -Method GET -version "beta" $from = $resu.id } catch { Write-Warning "User $from not found in the tenant" return } } if ($to -match ".+@.*\..+") { #if this is a upn we will use graph to get the objectID try { $resu = invoke-graph -endpoint "users/$to" -Method GET -version "beta" $to = $resu.id } catch { Write-Warning "User $to not found in the tenant" return } } $assignments=get-PIMAzureResourceEligibleAssignment -tenantID $tenantID -scope $scope -assignee $from $assignments | ForEach-Object { Write-Verbose "Copying assignment from $from to $to at scope $($_.scopeId) with role $($_.rolename)" New-PIMAzureResourceEligibleAssignment -tenantID $tenantID -subscriptionID $subscriptionID -scope $_.scopeId -rolename $_.rolename -principalID $to } } catch { MyCatch $_ } } <# .Synopsis Copy the setting of roles $copyfrom to the role $rolename .Description Copy the setting of roles $copyfrom to the role $rolename .Parameter tenantID EntraID tenant ID .Parameter subscriptionID subscription ID .Parameter rolename Array of the rolename to update .Parameter copyFrom We will copy the settings from this role to rolename .Example PS> Copy-PIMAzureResourcePolicy -subscriptionID "eedcaa84-3756-4da9-bf87-40068c3dd2a2" -rolename contributor,webmaster -copyFrom role1 Copy settings from role role1 to the contributor and webmaster roles .Link .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Copy-PIMAzureResourcePolicy { [CmdletBinding(DefaultParameterSetName='Default')] param ( [Parameter(Position = 0, Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] # Tenant ID $tenantID, [Parameter(ParameterSetName = 'Default',Position = 1, Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $subscriptionID, [Parameter(ParameterSetName = 'Scope',Position = 1, Mandatory = $true)] [System.String] $scope, [Parameter(Position = 2, Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String[]] $rolename, [Parameter(Position = 2, Mandatory = $true)] [ValidateNotNullOrEmpty()] $copyFrom ) try { $script:tenantID = $tenantID Write-Verbose "Copy-PIMAzureResourcePolicy start with parameters: tenantID => $tenantID subscription => $subscriptionID, rolename=> $rolename, copyfrom => $copyFrom" if (!($PSBoundParameters.Keys.Contains('scope'))) { $scope = "subscriptions/$subscriptionID" } $config2 = get-config $scope $copyFrom $true $rolename | ForEach-Object { $config = get-config $scope $_ Log "Copying settings from $copyFrom to $_" [string]$policyID = $config.policyID $policyID = $policyID.Trim() Update-Policy $policyID $config2 } } catch { MyCatch $_ } } <# .Synopsis Copy the setting of roles $copyfrom to the role $rolename .Description Copy the setting of roles $copyfrom to the role $rolename .Parameter tenantID EntraID tenant ID .Parameter rolename Array of the rolename to update .Parameter copyFrom We will copy the settings from this role to rolename .Example PS> Copy-PIMEntraRolePolicy -tenantID $tenantID -rolename contributor,webmaster -copyFrom role1 Copy settings from role role1 to the contributor and webmaster roles .Link .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Copy-PIMEntraRoleEligibleAssignment { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Position = 0, Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] # Tenant ID $tenantID, [Parameter(Position = 2, Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $from, [Parameter(Position = 2, Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $to ) try { #convert UPN to objectID if ($from -match ".+@.*\..+") { #if this is a upn we will use graph to get the objectID try { $resu = invoke-graph -endpoint "users/$from" -Method GET -version "beta" $from = $resu.id } catch { Write-Warning "User $from not found in the tenant" return } } if ($to -match ".+@.*\..+") { #if this is a upn we will use graph to get the objectID try { $resu = invoke-graph -endpoint "users/$to" -Method GET -version "beta" $to = $resu.id } catch { Write-Warning "User $to not found in the tenant" return } } $script:tenantID = $tenantID Write-Verbose "Copy-PIMEntraRoleAssignment start with parameters: tenantID => $tenantID from => $from, to=> $to" $assignements = Get-PIMEntraRoleEligibleAssignment -tenantid $tenantID #$assignements $assignements | Where-Object {$_.principalID -eq "$from"} | ForEach-Object { Write-Verbose ">>>New-PIMEntraRoleEligibleAssignment -tenantID $tenantID -roleName $($_.roleName) -principalID $to" New-PIMEntraRoleEligibleAssignment -tenantID $tenantID -roleName $_.roleName -principalID $to } } catch { MyCatch $_ } } <# .Synopsis Copy the setting of roles $copyfrom to the role $rolename .Description Copy the setting of roles $copyfrom to the role $rolename .Parameter tenantID EntraID tenant ID .Parameter rolename Array of the rolename to update .Parameter copyFrom We will copy the settings from this role to rolename .Example PS> Copy-PIMEntraRolePolicy -tenantID $tenantID -rolename contributor,webmaster -copyFrom role1 Copy settings from role role1 to the contributor and webmaster roles .Link .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Copy-PIMEntraRolePolicy { [CmdletBinding(DefaultParameterSetName='Default')] param ( [Parameter(Position = 0, Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] # Tenant ID $tenantID, [Parameter(Position = 2, Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String[]] $rolename, [Parameter(Position = 2, Mandatory = $true)] [ValidateNotNullOrEmpty()] $copyFrom ) try { $script:tenantID = $tenantID Write-Verbose "Copy-PIMEntraRolePolicy start with parameters: tenantID => $tenantID subscription => $subscriptionID, rolename=> $rolename, copyfrom => $copyFrom" export-PIMEntraRolepolicy -tenantid $tenantID -rolename $copyFrom -path "$env:TEMP\role.csv" $c=import-csv "$env:TEMP\role.csv" $rolename | ForEach-Object { #get policy id for current role and replace it in the csv before importing it $config = get-EntraRoleconfig $_ write-verbose "ID= $($config.PolicyID)" Log "Copying settings from $copyFrom to $_" [string]$policyID = $config.PolicyID $policyID = $policyID.Trim() write-verbose "before:$($c.policyID)" $c.PolicyID = $policyID $c |export-csv -Path "$env:TEMP\newrole.csv" -NoTypeInformation import-PIMEntraRolepolicy -tenantid $tenantID -path "$env:TEMP\newrole.csv" Remove-Item "$env:TEMP\role.csv" -Force Remove-Item "$env:TEMP\newrole.csv" -Force } } catch { MyCatch $_ } } <# .Synopsis EASYPIM Powershell module to manage PIM Azure Resource Role settings with simplicity in mind Get-PIMEntraRolePolicy will return the policy rules (like require MFA on activation) of the selected rolename at the subscription level Support querrying multi roles at once .Description Deny-PIMAzureResourcePendingApproval will deny request .PARAMETER approvalID approval ID from get-PIMAzureResourcePendingApproval .PARAMETER justification justification for the deny .Example PS> Deny-PIMAzureResourcePendingApproval -approvalID $approvalID -justification "You don't need this role" Deny a pending request .Link .Notes Homepage: https://github.com/kayasax/easyPIM Author: MICHEL, Loic Changelog: Todo: * allow other scopes #> function Deny-PIMAzureResourcePendingApproval { [CmdletBinding()] [OutputType([String])] param ( [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [System.String] # Tenant ID $approvalID, [Parameter(Position = 1, Mandatory = $true)] [System.String] # justification $justification ) process{ try { $script:tenantID = $tenantID Write-Verbose "approve-PIMAzureResourcePendingApproval start with parameters: approvalid => $approvalID, justification => $justification" #Get the stages: #Role Assignment Approval Steps - List - REST API (Azure Authorization) | Microsoft Learn $stages = Invoke-AzRestMethod -Uri "https://management.azure.com/$approvalID/stages?api-version=2021-01-01-preview" -Method GET $stageid = ($stages.Content | convertfrom-json).value.id #approve the request #Role Assignment Approval Step - Patch - REST API (Azure Authorization) | Microsoft Learn $body = '{"properties":{"justification":"' + $justification + '","reviewResult":"Deny"}}' Invoke-AzRestMethod -Uri "https://management.azure.com/$stageid/?api-version=2021-01-01-preview" -Payload $body -Method PUT return "Success, request denied" } catch { MyCatch $_ } }} <# .Synopsis EASYPIM Powershell module to manage PIM Azure Resource Role settings with simplicity in mind Get-PIMEntraRolePolicy will return the policy rules (like require MFA on activation) of the selected rolename at the subscription level Support querrying multi roles at once .Description Deny-PIMEntraRolePendingApprovall will use the Microsoft Graph APIs to retrieve the requests pending your approval .PARAMETER approvalID approval ID from get-PIMAzureResourcePendingApproval .PARAMETER justification justification for the approval .Example PS> Deny-PIMAzureResourcePendingApproval -approvalID $approvalID -justification "I Deny this request" Deny a pending request .Link .Notes Homepage: https://github.com/kayasax/easyPIM Author: MICHEL, Loic Changelog: Todo: * allow other scopes #> function Deny-PIMEntraRolePendingApproval { [CmdletBinding()] [OutputType([String])] param ( [Parameter(Position = 0, Mandatory = $true,ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [System.String] # Approval ID $approvalID, [Parameter(Position = 1, Mandatory = $true)] [System.String] # justification $justification ) process{ try { #$script:tenantID = $tenantID Write-Verbose "Deny-PIMEntraRolePendingApproval start with parameters: approvalid => $approvalID, justification => $justification" #Get the stages: #Role Assignment Approval Steps - List - REST API (Azure Authorization) | Microsoft Learn $stages=Invoke-graph -endpoint "roleManagement/directory/roleAssignmentApprovals/$approvalID/" -Method GET -version "beta" $stageid=$stages.id #Deny the request #Role Assignment Approval Step - Patch - REST API (Azure Authorization) | Microsoft Learn $body='{"justification":"'+$justification+'","reviewResult":"Deny"}' Invoke-graph -endpoint "roleManagement/directory/roleAssignmentApprovals/$approvalID/steps/$stageID" -body $body -version "beta" -Method PATCH return "Success, request Denyd" } catch { MyCatch $_ } }} <# .Synopsis EASYPIM Powershell module to manage PIM Azure Resource Role settings with simplicity in mind Get-PIMGroupPolicy will return the policy rules (like require MFA on activation) of the selected rolename at the subscription level Support querrying multi roles at once .Description Deny-PIMGroupPendingApprovall will use the Microsoft Graph APIs to retrieve the requests pending your approval .PARAMETER approvalID approval ID from get-PIMAzureResourcePendingApproval .PARAMETER justification justification for the approval .Example PS> Deny-PIMAzureResourcePendingApproval -approvalID $approvalID -justification "I Deny this request" Deny a pending request .Link .Notes Homepage: https://github.com/kayasax/easyPIM Author: MICHEL, Loic Changelog: Todo: * allow other scopes #> function Deny-PIMGroupPendingApproval { [CmdletBinding()] [OutputType([String])] param ( [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [System.String] # Approval ID $approvalID, [Parameter(Position = 1, Mandatory = $true)] [System.String] # justification $justification ) process { try { #$script:tenantID = $tenantID Write-Verbose "Deny-PIMGroupPendingApproval start with parameters: approvalid => $approvalID, justification => $justification" #Get the stages: #in groups stageID is the same as the approvalID #Deny the request #https://learn.microsoft.com/en-us/graph/api/approvalstage-update?view=graph-rest-1.0&tabs=http $body = '{"justification":"' + $justification + '","reviewResult":"Deny"}' Invoke-graph -endpoint "identityGovernance/privilegedAccess/group/assignmentApprovals/$approvalID/steps/$approvalID" -body $body -version "beta" -Method PATCH return "Success, request Denied" } catch { MyCatch $_ } } } <# .Synopsis Export the settings of the role $rolename at the subscription scope where subscription = $subscriptionID to $exportFilename, if not set file will be saved in %appdata%\powershell\EasyPIM\exports\ .Description Convert the policy rules to csv .Parameter tenantID EntraID tenant ID .Parameter subscriptionID subscription ID .Parameter rolename Array of the rolename to check .Parameter exportFilename Filename of the csv to genarate, if not specified default filename will be %appdata%\powershell\EasyPIM\Exports\<datetime>.csv .Example PS> Export-PIMAzureResourcePolicy -subscriptionID "eedcaa84-3756-4da9-bf87-40068c3dd2a2" -rolename contributor,webmaster -filename "c:\temp\myrole.csv" Export settings of contributor and webmaster roles to file c:\temp\myrole.csv .Link .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Export-PIMAzureResourcePolicy { [CmdletBinding(DefaultParameterSetName='Default')] param ( [Parameter(Position = 0, Mandatory = $true)] [System.String] $tenantID, [Parameter(ParameterSetName = 'Default',Position = 1, Mandatory = $true)] [System.String] $subscriptionID, [Parameter(ParameterSetName = 'Scope',Position = 1, Mandatory = $true)] [System.String[]] $scope, [Parameter(Position = 2, Mandatory = $true)] [System.String[]] $rolename, [Parameter(Position = 3)] [System.String] $exportFilename ) try { $script:tenantID = $tenantID Write-Verbose "Export-PIMAzureResourcePolicy start with parameters: subscription => $subscriptionID, rolename=> $rolename, exportFilname => $exportFilename" if (!($PSBoundParameters.Keys.Contains('scope'))) { $scope = "subscriptions/$subscriptionID" } # Array to contain the settings of each selected roles $exports = @() # run the flow for each role name. $rolename | ForEach-Object { #get curent config $config = get-config $scope $_ $exports += $config } $date = get-date -Format FileDateTime if (!($exportFilename)) { $exportFilename = "$script:_logPath\EXPORTS\$date.csv" } log "exporting to $exportFilename" $exportPath = Split-Path $exportFilename -Parent #create export folder if no exist if ( !(test-path $exportFilename) ) { $null = New-Item -ItemType Directory -Path $exportPath -Force } $exports | Select-Object * | ConvertTo-Csv | out-file $exportFilename log "Success! Script ended normaly" } catch { MyCatch $_ } } <# .Synopsis Export the settings of the role $rolename to csv .Description Convert the policy rules to csv .Parameter tenantID EntraID tenant ID .Parameter rolename Array of the rolename to check .Parameter path path of the csv to genarate, if not specified default filename will be %appdata%\powershell\EasyPIM\Exports\EntraRoles_<datetime>.csv .Example PS> Export-PIMEntraRolePolicy -tenantID $tenantID -rolename "Global Reader","Directory Writers" -path "c:\temp\role.csv" Export settings of "Global Reader" and "Directory Writers" roles to file c:\temp\role.csv .Link .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Export-PIMEntraRolePolicy { [CmdletBinding(DefaultParameterSetName='Default')] param ( [Parameter(Position = 0, Mandatory = $true)] [System.String] $tenantID, [Parameter(Position = 1, Mandatory = $true)] [System.String[]] $rolename, [Parameter(Position = 2)] [System.String] $path ) try { $script:tenantID = $tenantID Write-Verbose "Export-PIMEntraRolePolicy start with parameters: subscription => $subscriptionID, rolename=> $rolename, exportFilname => $path" # Array to contain the settings of each selected roles $exports = @() # run the flow for each role name. $rolename | ForEach-Object { #get curent config $config = get-EntraRoleconfig $_ $exports += $config } $date = get-date -Format FileDateTime if (!($path)) { $path = "$script:_logPath\EXPORTS\EntraRoles_$date.csv" } log "exporting to $path" $exportPath = Split-Path $path -Parent #create export folder if no exist if ( !(test-path $path) ) { $null = New-Item -ItemType Directory -Path $exportPath -Force } $exports | Select-Object * | ConvertTo-Csv | out-file $path log "Success! Script ended normaly" } catch { MyCatch $_ } } <# .Synopsis List of active assignement defined at the provided scope or bellow .Description Active assignment does not require to activate their role. https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Parameter tenantID EntraID tenant ID .Parameter subscriptionID subscription ID .Parameter scope use scope parameter if you want to work at other scope than a subscription .Parameter summary When enabled will return the most useful information only .Parameter atBellowScope Will return only the assignment defined at lower scopes .Example PS> Get-PIMAzureResourceActiveAssignment -tenantID $tid -subscriptionID -subscription $subscription List active assignement at the subscription scope. .Link https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Get-PIMAzureResourceActiveAssignment { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [String] $tenantID, [Parameter(Position = 1)] [String] $subscriptionID, [Parameter()] [String] $scope, [switch] # select the most usefull info only $summary, [switch] # return only assignment defined at a lower scope $atBellowScope ) try { if (!($PSBoundParameters.Keys.Contains('scope'))) { $scope = "/subscriptions/$subscriptionID" } # Issue #23due to a bug with the API regarding the membertype, we will use RoleAssignmentSchedulesInstance instead of RoleAssignmentsSchedule # the downside is we will not get assignment with a future start date #$restURI = "https://management.azure.com/$scope/providers/Microsoft.Authorization/roleAssignmentSchedules?api-version=2020-10-01" $restURI = "https://management.azure.com/$scope/providers/Microsoft.Authorization/roleAssignmentScheduleInstances?api-version=2020-10-01" $script:tenantID = $tenantID $response = Invoke-ARM -restURI $restURI -method get #$response|select -first 1 $return = @() #$id=$response.value.id #$response.value.properties |get-member $response.value | ForEach-Object { $id = $_.id #echo "ID: $id" $_.properties | ForEach-Object { #$_ if ($null -eq $_.endDateTime ) { $end = "permanent" }else { $end = $_.endDateTime } $properties = @{ "PrincipalName" = $_.expandedproperties.principal.displayName "PrincipalEmail" = $_.expandedproperties.principal.email; "PrincipalType" = $_.expandedproperties.principal.type; "PrincipalId" = $_.expandedproperties.principal.id; "RoleName" = $_.expandedproperties.roleDefinition.displayName; "RoleType" = $_.expandedproperties.roleDefinition.type; "RoleId" = $_.expandedproperties.roleDefinition.id; "ScopeId" = $_.expandedproperties.scope.id; "ScopeName" = $_.expandedproperties.scope.displayName; "ScopeType" = $_.expandedproperties.scope.type; "Status" = $_.Status; "createdOn" = $_.createdOn "startDateTime" = $_.startDateTime "endDateTime" = $end "updatedOn" = $_.updatedOn "memberType" = $_.memberType "id" = $id } $obj = New-Object pscustomobject -Property $properties $return += $obj } } if ($PSBoundParameters.Keys.Contains('summary')) { $return = $return | Select-Object scopeid, rolename, roletype, principalid, principalName, principalEmail, PrincipalType, status, startDateTime, endDateTime } if ($PSBoundParameters.Keys.Contains('atBellowScope')) { $return = $return | Where-Object { $($_.scopeid).Length -gt $scope.Length } } return $return } catch { Mycatch $_ } } <# .Synopsis List of eligible assignement defined at the provided scope or bellow .Description https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Parameter tenantID EntraID tenant ID .Parameter subscriptionID subscription ID .Parameter scope use scope parameter if you want to work at other scope than a subscription .PARAMETER assignee Filter assignment using userprincipalname or objectID .Parameter summary When enabled will return the most useful information only .Parameter atBellowScope Will return only the assignment defined at lower scopes .Example PS> Get-PIMAzureResourceEligibleAssignment -tenantID $tid -subscriptionID -subscription $subscription List active assignement at the subscription scope. .Link https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Get-PIMAzureResourceEligibleAssignment { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [String] $tenantID, [Parameter(Position = 1)] [String] $subscriptionID, [Parameter()] [String] $scope, [String] $assignee, [switch] # when enable we will use the roleEligibilitySchedules API which also list the future assignments $includeFutureAssignments, [switch] # select the most usefull info only $summary, [switch] # return only assignment defined at a lower scope $atBellowScope ) try { $script:tenantID = $tenantID if (!($PSBoundParameters.Keys.Contains('scope'))) { $scope = "/subscriptions/$subscriptionID" } # issue #23: due to a bug with the API regarding the membertype, we will use RoleEligibilitySchedulesInstance instead of RoleEligibilitySchedule # the downside is we will not get assignment with a future start date if ($PSBoundParameters.Keys.Contains('includeFutureAssignments')) { $restURI = "https://management.azure.com/$scope/providers/Microsoft.Authorization/roleEligibilitySchedules?api-version=2020-10-01" } else { $restURI = "https://management.azure.com/$scope/providers/Microsoft.Authorization/roleEligibilityScheduleInstances?api-version=2020-10-01" } #issue #70 filter assignment of a specific user if ($PSBoundParameters.Keys.Contains('assignee')) { if($assignee -match ".+@.*\..+") { #if this is a upn we will use graph to get the objectID try{ $resu=invoke-graph -endpoint "users/$assignee" -Method GET -version "beta" $assignee = $resu.id } catch { Write-Warning "User $assignee not found in the tenant" return } } $restURI += "&`$filter=assignedto('"+$assignee+"')" } $response = Invoke-ARM -restURI $restURI -method get #$response|select -first 1 $return = @() #$id=$response.value.id #$response.value.properties |get-member $response.value | ForEach-Object { $id = $_.id #echo "ID: $id" $_.properties | ForEach-Object { #$_ if ($null -eq $_.endDateTime ) { $end = "permanent" }else { $end = $_.endDateTime } $properties = @{ "PrincipalName" = $_.expandedproperties.principal.displayName "PrincipalEmail" = $_.expandedproperties.principal.email; "PrincipalType" = $_.expandedproperties.principal.type; "PrincipalId" = $_.expandedproperties.principal.id; "RoleName" = $_.expandedproperties.roleDefinition.displayName; "RoleType" = $_.expandedproperties.roleDefinition.type; "RoleId" = $_.expandedproperties.roleDefinition.id; "ScopeId" = $_.expandedproperties.scope.id; "ScopeName" = $_.expandedproperties.scope.displayName; "ScopeType" = $_.expandedproperties.scope.type; "Status" = $_.Status; "createdOn" = $_.createdOn "startDateTime" = $_.startDateTime "endDateTime" = $end "updatedOn" = $_.updatedOn "memberType" = $_.memberType "id" = $id } $obj = New-Object pscustomobject -Property $properties $return += $obj } } if ($PSBoundParameters.Keys.Contains('summary')) { $return = $return | Select-Object scopeid, rolename, roletype, principalid, principalName, principalEmail, PrincipalType, status, startDateTime, endDateTime } if ($PSBoundParameters.Keys.Contains('atBellowScope')) { $return = $return | Where-Object { $($_.scopeid).Length -gt $scope.Length } } return $return } catch { MyCatch $_ } } <# .Synopsis EASYPIM Powershell module to manage PIM Azure Resource Role settings with simplicity in mind Get-PIMEntraRolePolicy will return the policy rules (like require MFA on activation) of the selected rolename at the subscription level Support querrying multi roles at once .Description Get-PIMAzureResourcePendingApproval will use the Microsoft Graph APIs to retrieve the requests pending your approval .PARAMETER tenantID Tenant ID .Example PS> Get-PIMAzureResourcePendingApproval -tenantID $tenantID show pending request you can approve .Link .Notes Homepage: https://github.com/kayasax/easyPIM Author: MICHEL, Loic Changelog: Todo: * allow other scopes #> function Get-PIMAzureResourcePendingApproval { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")] [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [System.String] # Tenant ID $tenantID ) try { $script:tenantID = $tenantID Write-Verbose "Get-PIMAzureResourcePendingApproval start with parameters: tenantID => $tenantID" $out = @() $response = invoke-AzRestMethod -Uri "https://management.azure.com/providers/Microsoft.Authorization/roleAssignmentScheduleRequests?api-version=2020-10-01&`$filter=asApprover()" $pendingApproval = $response.Content | convertfrom-json if ($null -ne $pendingApproval.value.properties) { $pendingApproval.value.properties | ForEach-Object { $request = @{ "principalType" = $_.principalType; "principalId" = $_.expandedProperties.Principal.id; "principalDisplayname" = $_.expandedProperties.Principal.displayName; "roleId" = $_.expandedProperties.RoleDefinition.id; "roleDisplayname" = $_.expandedProperties.RoleDefinition.displayName; "status" = $_.status; "startDateTime" = $_.scheduleInfo.startDateTime; "ticketInfo" = $_.ticketInfo; "justification" = $_.justification; "scope" = $_.Scope; "approvalId" = $_.approvalId; "requestType" = $_.requestType; "createdOn" = $_.createdOn; } $o = New-Object -TypeName PSObject -Property $request $out += $o } } if ($out.length -eq 0) { #write-host "No pending approval" return $null } return $out } catch { MyCatch $_ } } <# .Synopsis EASYPIM Powershell module to manage PIM Azure Resource Role settings with simplicity in mind Get-PIMAzureResourcePolicy will return the policy rules (like require MFA on activation) of the selected rolename at the subscription level Support querrying multi roles at once .Description Get-PIMAzureResourcePolicy will use the ARM REST APIs to retrieve the settings of the role at the subscription scope .PARAMETER tenantID Tenant ID .PARAMETER subscriptionID Subscription ID .PARAMETER rolename Name of the role to check .Example PS> Get-PIMAzureResourcePolicy -tenantID $tenantID -subscriptionID $subscriptionID -rolename "contributor","webmaster" show curent config for the roles contributor and webmaster at the subscriptionID scope : .Link https://learn.microsoft.com/en-us/azure/governance/resource-graph/first-query-rest-api https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview Duration ref https://en.wikipedia.org/wiki/ISO_8601#Durations .Notes Homepage: https://github.com/kayasax/easyPIM Author: MICHEL, Loic Changelog: Todo: * allow other scopes #> function Get-PIMAzureResourcePolicy { [CmdletBinding(DefaultParameterSetName='Default')] [OutputType([PSCustomObject])] param ( [Parameter(Position = 0, Mandatory = $true)] [System.String] # Tenant ID $tenantID, [Parameter(ParameterSetName = 'Default',Position = 1, Mandatory = $true)] [System.String] # Subscription ID $subscriptionID, [Parameter(ParameterSetName = 'Scope',Position = 1, Mandatory = $true)] [System.String] $scope, [Parameter(Position = 2, Mandatory = $true)] [System.String[]] # Array of role name $rolename ) try { $script:tenantID = $tenantID Write-Verbose "Get-PIMAzureResourcePolicy start with parameters: subscription => $subscriptionID, rolename=> $rolename" #defaut scope = subscription if (!($PSBoundParameters.Keys.Contains('scope'))) { $scope = "subscriptions/$subscriptionID" } $out = @() $rolename | ForEach-Object { #get curent config $config = get-config $scope $_ $out += $config } Write-Output $out -NoEnumerate } catch { MyCatch $_ } } <# .Synopsis List of PIM Entra Role active assignement .Description Active assignment does not require to activate their role. https://learn.microsoft.com/en-us/graph/api/rbacapplication-list-roleeligibilityscheduleinstances?view=graph-rest-1.0&tabs=http .Parameter tenantID EntraID tenant ID .Parameter summary When enabled will return the most useful information only .PARAMETER rolename Filter by rolename .PARAMETER principalid Filter by principalid .PARAMETER principalName Filter by principalName .Example PS> Get-PIMEntraRoleActiveAssignment -tenantID $tid -rolename "testrole" -principalName "loic" List active assignement for role "testrole" and user name "loic" .Link .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Get-PIMEntraRoleActiveAssignment { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [String] $tenantID, # select the most usefull info only [switch]$summary, [string]$principalid, [string]$rolename, [string]$principalName ) try { $script:tenantID = $tenantID $endpoint = "roleManagement/directory/roleAssignmentScheduleInstances?`$expand=roleDefinition,principal" $response = invoke-graph -Endpoint $endpoint $resu = @() $response.value | ForEach-Object { $r = @{ "rolename" = $_.roledefinition.displayName "roleid" = $_.roledefinition.id "principalname" = $_.principal.displayName "principalid" = $_.principal.id "principalEmail" = $_.principal.mail "startDateTime" = $_.startDateTime "endDateTime" = $_.endDateTime "directoryScopeId" = $_.directoryScopeId "memberType" = $_.memberType "assignmentType" = $_.assignmentType #"activatedUsing"=$_.activatedUsing "principaltype" = $_.principal."@odata.type" "id" = $_.id } $resu += New-Object PSObject -Property $r } if ($PSBoundParameters.Keys.Contains('summary')) { $resu = $resu | Select-Object rolename, roleid, principalid, principalName, principalEmail, PrincipalType, startDateTime, endDateTime, directoryScopeId } if ($PSBoundParameters.Keys.Contains('principalid')) { $resu = $resu | Where-Object { $_.principalid -eq $principalid } } if ($PSBoundParameters.Keys.Contains('rolename')) { $resu = $resu | Where-Object { $_.rolename -eq $rolename } } if($PSBoundParameters.Keys.Contains('principalName')){ $resu = $resu | Where-Object { $_.principalName -match $principalName } } return $resu } catch { MyCatch $_ } } <# .Synopsis List of PIM Entra Role active assignement .Description Active assignment does not require to activate their role. https://learn.microsoft.com/en-us/graph/api/rbacapplication-list-roleeligibilityscheduleinstances?view=graph-rest-1.0&tabs=http .Parameter tenantID EntraID tenant ID .Parameter summary When enabled will return the most useful information only .PARAMETER rolename Filter by rolename .PARAMETER principalid Filter by principalid .PARAMETER principalName Filter by principalName .Example PS> Get-PIMEntraRoleEligibleAssignment -tenantID $tid List active assignement .Link .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Get-PIMEntraRoleEligibleAssignment { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [String] $tenantID, # select the most usefull info only [switch]$summary, [string]$principalid, [string]$rolename, [string]$principalName ) try { $script:tenantID = $tenantID $endpoint = "/roleManagement/directory/roleEligibilityScheduleInstances?`$expand=roleDefinition,principal" $response = invoke-graph -Endpoint $endpoint $resu = @() $response.value | ForEach-Object { $r = @{ "rolename" = $_.roledefinition.displayName "roleid" = $_.roledefinition.id "principalname" = $_.principal.displayName "principalid" = $_.principal.id "startDateTime" = $_.startDateTime "endDateTime" = $_.endDateTime "directoryScopeId" = $_.directoryScopeId "memberType" = $_.memberType "assignmentType" = $_.assignmentType #"activatedUsing"=$_.activatedUsing "type" = $_.principal."@odata.type" "id" = $_.id } $resu += New-Object PSObject -Property $r } if ($PSBoundParameters.Keys.Contains('summary')) { $resu = $resu | Select-Object rolename, roleid, principalid, principalName, principalEmail, PrincipalType, startDateTime, endDateTime, directoryScopeId } if ($PSBoundParameters.Keys.Contains('principalid')) { $resu = $resu | Where-Object { $_.principalid -eq $principalid } } if ($PSBoundParameters.Keys.Contains('rolename')) { $resu = $resu | Where-Object { $_.rolename -eq $rolename } } if($PSBoundParameters.Keys.Contains('principalName')){ $resu = $resu | Where-Object { $_.principalName -match $principalName } } return $resu } catch { Mycatch $_ } } <# .Synopsis EASYPIM Powershell module to manage PIM Azure Resource Role settings with simplicity in mind Get-PIMEntraRolePolicy will return the policy rules (like require MFA on activation) of the selected rolename at the subscription level Support querrying multi roles at once .Description Get-PIMEntraRolePendingApproval will use the Microsoft Graph APIs to retrieve the requests pending your approval .PARAMETER tenantID Tenant ID .Example PS> Get-PIMEntraRolePendingApproval -tenantID $tenantID show pending request you can approve .Link .Notes Homepage: https://github.com/kayasax/easyPIM Author: MICHEL, Loic Changelog: Todo: * allow other scopes #> function Get-PIMEntraRolePendingApproval{ [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")] [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [System.String] # Tenant ID $tenantID ) try { $script:tenantID = $tenantID Write-Verbose "Get-PIMAzureResourcePendingApproval start with parameters: tenantID => $tenantID" $endpoint="/roleManagement/directory/roleAssignmentScheduleRequests/filterByCurrentUser(on='approver')?`$filter=status eq 'PendingApproval'" $response = Invoke-Graph -Endpoint $endpoint -Method "GET" $out = @() $pendingApproval = $response.value if ($null -ne $pendingApproval) { $pendingApproval | ForEach-Object { $role=invoke-mgGraphRequest $("https://graph.microsoft.com/v1.0/directoryRoles(roletemplateid ='"+$_.roledefinitionid+"')") -Method get $principalDisplayName = invoke-mgGraphRequest $("https://graph.microsoft.com/v1.0/directoryobjects/"+$_.Principalid+"/") -Method get $request = @{ "principalId" = $_.Principalid; "principalDisplayname" = $principalDisplayName.displayName; "roleId" = $_.RoleDefinitionid; "roleDisplayname" = $role.displayname; "status" = $_.status; "startDateTime" = $_.CreatedDateTime; "ticketInfo" = $_.ticketInfo; "justification" = $_.justification; "scope" = "/"; "approvalId" = $_.approvalId; "createdOn" = $_.createdDateTime; } $o = New-Object -TypeName PSObject -Property $request $out += $o } } if ($out.length -eq 0) { #write-host "No pending approval" return $null } return $out } catch { MyCatch $_ } } <# .Synopsis EASYPIM Powershell module to manage PIM Azure Resource Role settings with simplicity in mind Get-PIMEntraRolePolicy will return the policy rules (like require MFA on activation) of the selected rolename at the subscription level Support querrying multi roles at once .Description Get-PIMEntraRolePolicy will use the Microsoft Graph APIs to retrieve the PIM settings of the role $rolename .PARAMETER tenantID Tenant ID .PARAMETER rolename Name of the role to check .Example PS> Get-PIMEntraRolePolicy -tenantID $tenantID -rolename "Global Administrator","Global Reader" show curent config for the roles global administrator and global reader .Link https://learn.microsoft.com/en-us/azure/governance/resource-graph/first-query-rest-api https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview Duration ref https://en.wikipedia.org/wiki/ISO_8601#Durations .Notes Homepage: https://github.com/kayasax/easyPIM Author: MICHEL, Loic Changelog: Todo: * allow other scopes #> function Get-PIMEntraRolePolicy { [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Position = 0, Mandatory = $true)] [System.String] # Tenant ID $tenantID, [Parameter(Position = 1, Mandatory = $true)] [System.String[]] # Array of role name $rolename ) try { $script:tenantID = $tenantID Write-Verbose "Get-PIMEntraRolePolicy start with parameters: tenantID => $tenantID, rolename=> $rolename" $out = @() $rolename | ForEach-Object { #get curent config $config = get-EntraRoleConfig $_ $out += $config } Write-Output $out -NoEnumerate } catch { MyCatch $_ } } <# .Synopsis List active assignements for a group .Description Active assignment does not require to activate their role. https://learn.microsoft.com/en-us/graph/api/rbacapplication-list-roleeligibilityscheduleinstances?view=graph-rest-1.0&tabs=http .Parameter tenantID EntraID tenant ID .PARAMETER groupID The group id to check .PARAMETER memberType Filter results by memberType (owner or member) .PARAMETER principalName Filter results by principalName starting with the given value .Parameter summary When enabled will return the most useful information only .Example PS> Get-PIMGroupActiveAssignment -tenantID $tid -groupID $gID List active assignement for the group $gID .Example PS> Get-PIMGroupActiveAssignment -tenantID $tid -groupID $gID -memberType owner -principalName "loic" -summary Get a summary of the active assignement for the group $gID, for the owner role and for the user "loic" .Link .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Get-PIMGroupActiveAssignment { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [String] $tenantID, [Parameter(Mandatory = $true)] [string]$groupID, [string]$memberType, [string]$principalName, [switch]$summary ) try { $script:tenantID = $tenantID $endpoint = "identityGovernance/privilegedAccess/group/assignmentSchedules?`$filter=groupId eq '$groupID'&`$expand=principal " $response = invoke-graph -Endpoint $endpoint $resu = @() $response.value | ForEach-Object { $r = @{ "principalname" = $_.principal.displayName "principalid" = $_.principal.id "principalEmail" = $_.principal.mail "startDateTime" = $_.scheduleInfo.startDateTime "endDateTime" = $_.scheduleInfo.expiration.endDateTime "memberType" = $_.accessId "assignmentType" = $_.memberType "principaltype" = $_.principal."@odata.type" "id" = $_.id } $resu += New-Object PSObject -Property $r } if ($PSBoundParameters.Keys.Contains('summary')) { $resu = $resu | Select-Object rolename, roleid, principalid, principalName, principalEmail, PrincipalType, startDateTime, endDateTime, directoryScopeId } if ($PSBoundParameters.Keys.Contains('principalid')) { $resu = $resu | Where-Object { $_.principalid -eq $principalid } } if ($PSBoundParameters.Keys.Contains('memberType')) { $resu = $resu | Where-Object { $_.memberType -eq $memberType } } if($PSBoundParameters.Keys.Contains('principalName')){ $resu = $resu | Where-Object { $_.principalName -match $principalName } } return $resu } catch { MyCatch $_ } } <# .Synopsis List of PIM Entra Role active assignement .Description Active assignment does not require to activate their role. https://learn.microsoft.com/en-us/graph/api/rbacapplication-list-roleeligibilityscheduleinstances?view=graph-rest-1.0&tabs=http .Parameter tenantID EntraID tenant ID .Parameter summary When enabled will return the most useful information only .PARAMETER rolename Filter by rolename .PARAMETER principalid Filter by principalid .PARAMETER principalName Filter by principalName .Example PS> Get-PIMEntraRoleActiveAssignment -tenantID $tid List active assignement .Link .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Get-PIMGroupEligibleAssignment { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [String] $tenantID, # select the most usefull info only [switch]$summary, [Parameter(Mandatory = $true)] [string]$groupID, [string]$rolename, [string]$principalName ) try { $script:tenantID = $tenantID $endpoint = "identityGovernance/privilegedAccess/group/eligibilitySchedules?`$filter=groupId eq '$groupID'&`$expand=principal " $response = invoke-graph -Endpoint $endpoint $resu = @() $response.value | ForEach-Object { $r = @{ #"rolename" = $_.roledefinition.displayName ##"roleid" = $_.roledefinition.id "principalname" = $_.principal.displayName "principalid" = $_.principal.id "principalEmail" = $_.principal.mail "startDateTime" = $_.scheduleInfo.startDateTime "endDateTime" = $_.scheduleInfo.expiration.endDateTime #"directoryScopeId" = $_.directoryScopeId "memberType" = $_.accessId "assignmentType" = $_.memberType #"activatedUsing"=$_.activatedUsing "principaltype" = $_.principal."@odata.type" "id" = $_.id } $resu += New-Object PSObject -Property $r } if ($PSBoundParameters.Keys.Contains('summary')) { $resu = $resu | Select-Object rolename, roleid, principalid, principalName, principalEmail, PrincipalType, startDateTime, endDateTime, directoryScopeId } if ($PSBoundParameters.Keys.Contains('principalid')) { $resu = $resu | Where-Object { $_.principalid -eq $principalid } } if ($PSBoundParameters.Keys.Contains('rolename')) { $resu = $resu | Where-Object { $_.rolename -eq $rolename } } if($PSBoundParameters.Keys.Contains('principalName')){ $resu = $resu | Where-Object { $_.principalName -match $principalName } } return $resu } catch { MyCatch $_ } } <# .Synopsis EASYPIM Powershell module to manage PIM Azure Resource Role settings with simplicity in mind Get-PIMGroupPolicy will return the policy rules (like require MFA on activation) of the selected rolename at the subscription level Support querrying multi roles at once .Description Get-PIMGroupPendingApproval will use the Microsoft Graph APIs to retrieve the requests pending your approval .PARAMETER tenantID Tenant ID .Example PS> Get-PIMGroupPendingApproval -tenantID $tenantID show pending request you can approve .Link .Notes Homepage: https://github.com/kayasax/easyPIM Author: MICHEL, Loic Changelog: Todo: * allow other scopes #> function Get-PIMGroupPendingApproval{ [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")] [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [System.String] # Tenant ID $tenantID ) try { $script:tenantID = $tenantID Write-Verbose "Get-PIMAzureResourcePendingApproval start with parameters: tenantID => $tenantID" $endpoint="identityGovernance/privilegedAccess/group/assignmentScheduleRequests/filterByCurrentUser(on='approver')?`$filter=status eq 'PendingApproval'" $response = Invoke-Graph -Endpoint $endpoint -Method "GET" $out = @() $pendingApproval = $response.value if ($null -ne $pendingApproval) { $pendingApproval | ForEach-Object { $details=invoke-mgGraphRequest $("https://graph.microsoft.com/v1.0/identityGovernance/privilegedAccess/group/assignmentScheduleRequests/"+$_.id) -Method get #$details $principalDisplayName = invoke-mgGraphRequest $("https://graph.microsoft.com/v1.0/directoryobjects/"+$details.Principalid+"/") -Method get $groupDisplayName = invoke-mgGraphRequest $("https://graph.microsoft.com/v1.0/directoryobjects/"+$details.Groupid+"/") -Method get $request = @{ "principalId" = $details.Principalid; "principalDisplayname" = $principalDisplayName.displayName; "groupId" = $details.groupId; "groupDisplayname" = $groupDisplayName.displayName; "role" = $details.AccessID; "status" = $details.status; "startDateTime" = $details.CreatedDateTime; "ticketInfo" = $details.ticketInfo; "justification" = $details.justification; "approvalId" = $details.approvalId; "createdOn" = $details.createdDateTime; } $o = New-Object -TypeName PSObject -Property $request $out += $o } } if ($out.length -eq 0) { #write-host "No pending approval" return $null } return $out } catch { MyCatch $_ } } <# .Synopsis Get member or owner PIM settings for a group .Description Get member or owner PIM settings for a group .PARAMETER tenantID Tenant ID .PARAMETER GroupID Id of the group to check .PARAMETER GroupName Search for the group by name .PARAMETER type owner or member .Example PS> Get-PIMGroupPolicy -tenantID $tenantID -groupID $gID -type member show curent config for the member role of the group $gID .Example PS> Get-PIMGroupPolicy -tenantID $tenantID -groupname "Mygroup" -type owner show curent config for the owner role of the group "Mygroup" .Link https://learn.microsoft.com/en-us/azure/governance/resource-graph/first-query-rest-api https://learn.microsoft.com/en-us/graph/identity-governance-pim-rules-overview Duration ref https://en.wikipedia.org/wiki/ISO_8601#Durations .Notes Homepage: https://github.com/kayasax/easyPIM Author: MICHEL, Loic Changelog: #> function Get-PIMGroupPolicy { [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Position = 0, Mandatory = $true)] [System.String] # Tenant ID $tenantID, [Parameter(Position = 1)] [System.String[]] # Array of role name $groupID, [Parameter(Position = 2)] [System.String] # Array of role name $groupName, [Parameter(Mandatory = $true)] [System.String] #owner or member $type ) try { $script:tenantID = $tenantID if ($PSBoundParameters.ContainsKey('groupname')) { $endpoint="/groups?`$filter=startswith(displayName,'$($groupName)')" $response=invoke-graph -Endpoint $endpoint $groupID+=$response.value.id } #fix #77 elseif (!( $PSBoundParameters.ContainsKey('groupID'))) { throw "You must provide a groupID or a groupName" } $out = @() $groupID | ForEach-Object { #get curent config $config = get-GroupConfig $_ $type $out += $config } Write-Output $out -NoEnumerate } catch { MyCatch $_ } } <# .Synopsis Import the settings from the csv file $path .Description Convert the csv back to policy rules .Parameter tenantID Entra ID Tenant ID .Parameter subscriptionID subscription ID .Parameter Path path to the csv file .Example PS> Import-PIMAzureResourcePolicy -tenantID $tenantID -subscriptionID $subscriptionID -path "c:\temp\myrole.csv" Import settings from file c:\temp\myrole.csv .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Import-PIMAzureResourcePolicy { [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $TenantID, [Parameter(Mandatory = $true)] [String] $Path ) try { $script:tenantID = $TenantID #load settings Write-Verbose "Importing settings from $path" if ($PSCmdlet.ShouldProcess($path, "Importing policy from")) { import-setting $Path } Log "Success, exiting." } catch { Mycatch $_ } } <# .Synopsis Import the settings from the csv file $path .Description Convert the csv back to policy rules .Parameter tenantID Entra ID Tenant ID .Parameter Path path to the csv file .Example PS> Import-PIMEntraRolePolicy -tenantID $tenantID -path "c:\temp\myrole.csv" Import settings from file c:\temp\myrole.csv .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Import-PIMEntraRolePolicy { [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [System.String] $TenantID, [Parameter(Mandatory = $true)] [String] $Path ) try{ $script:tenantID = $TenantID #load settings Write-Verbose "Importing settings from $path" if ($PSCmdlet.ShouldProcess($path, "Importing policy from")) { Import-EntraRoleSettings $Path } Log "Success, exiting." } catch { Mycatch $_ } } function Invoke-EasyPIMOrchestrator { [CmdletBinding(DefaultParameterSetName = 'Default', SupportsShouldProcess = $true)] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] param ( [Parameter(Mandatory = $true, ParameterSetName = 'KeyVault')] [string]$KeyVaultName, [Parameter(Mandatory = $true, ParameterSetName = 'KeyVault')] [string]$SecretName, [Parameter(Mandatory = $true)] [string]$SubscriptionId, [Parameter(Mandatory = $true, ParameterSetName = 'FilePath')] [string]$ConfigFilePath, [Parameter(Mandatory = $false)] [ValidateSet("initial", "delta")] [string]$Mode = "delta", [Parameter(Mandatory = $true)] [string]$TenantId ) Write-SectionHeader "Starting EasyPIM Orchestration (Mode: $Mode)" # Display usage if no parameters are provided if (-not $PSBoundParameters) { Show-EasyPIMUsage return } try { # Import necessary modules Write-Host "Importing required modules..." -ForegroundColor Gray Import-Module Az.KeyVault, Az.Resources # 1. Load configuration $config = if ($PSCmdlet.ParameterSetName -eq 'KeyVault') { Get-EasyPIMConfiguration -KeyVaultName $KeyVaultName -SecretName $SecretName } else { Get-EasyPIMConfiguration -ConfigFilePath $ConfigFilePath } # 2. Process and normalize config $processedConfig = Initialize-EasyPIMAssignments -Config $config # 3. Perform cleanup operations $cleanupResults = Invoke-EasyPIMCleanup -Config $processedConfig -Mode $Mode -TenantId $TenantId -SubscriptionId $SubscriptionId # 4. Process assignments $assignmentResults = New-EasyPIMAssignments -Config $processedConfig -TenantId $TenantId -SubscriptionId $SubscriptionId # 5. Display summary Write-EasyPIMSummary -CleanupResults $cleanupResults -AssignmentResults $assignmentResults Write-Host "=== EasyPIM orchestration completed successfully ===" -ForegroundColor Green } catch { Write-Error "❌ An error occurred: $($_.Exception.Message)" Write-Verbose "Stack trace: $($_.ScriptStackTrace)" throw } } <# .Synopsis Create an active assignement at the provided scope .Description Active assignment does not require users to activate their role. https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Parameter tenantID EntraID tenant ID .Parameter subscriptionID subscription ID .Parameter scope use scope parameter if you want to work at other scope than a subscription .Parameter principalID objectID of the principal (user, group or service principal) .Parameter rolename name of the role to assign .Parameter duration duration of the assignment, if not set we will use the maximum allowed value from the role policy .Parameter startDateTime When the assignment wil begin, if not set we will use current time .Parameter permanent Use this parameter if you want a permanent assignement (no expiration) .Parameter justification justification .Example PS> New-PIMAzureResourceActiveAssigment -tenantID $tenantID -subscriptionID $subscriptionId -rolename "AcrPush" -principalID 3604fe63-cb67-4b60-99c9-707d46ab9092 -startDateTime "2/2/2024 18:20" Create an active assignment fot the role Arcpush, starting at a specific date and using default duration PS> New-PIMAzureResourceActiveAssigment -tenantID $tenantID -subscriptionID $subscriptionId -rolename "webmaster" -principalID 3604fe63-cb67-4b60-99c9-707d46ab9092 -justification 'TEST' -permanent Create a permanent active assignement for the role webmaster .Link https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function New-PIMAzureResourceActiveAssignment { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [String] # Entra ID tenantID $tenantID, [Parameter(Position = 1)] [String] # subscription ID $subscriptionID, [Parameter()] [String] # scope if not at the subscription level $scope, [Parameter(Mandatory = $true)] [String] # Principal ID $principalID, [Parameter(Mandatory = $true)] [string] # the rolename for which we want to create an assigment $rolename, [string] # duration of the assignment, if not set we will use the maximum allowed value from the role policy $duration, [string] # stat date of assignment if not provided we will use curent time $startDateTime, [string] # justification (will be auto generated if not provided) $justification, [switch] # the assignment will not expire $permanent ) try{ if (!($PSBoundParameters.Keys.Contains('scope'))) { if (!($PSBoundParameters.Keys.Contains('subscriptionID'))) { throw "ERROR : You must provide a subsciption ID or a scope, exiting." } $scope = "/subscriptions/$subscriptionID" } $script:tenantID = $tenantID $ARMhost = "https://management.azure.com" $ARMendpoint = "$ARMhost/$scope/providers/Microsoft.Authorization" #1 get role id $restUri = "$ARMendpoint/roleDefinitions?api-version=2022-04-01&`$filter=roleName eq '$rolename'" $response = Invoke-ARM -restURI $restUri -method "get" -body $null $roleID = $response.value.id write-verbose "Getting role ID for $rolename at $restURI" write-verbose "role ID = $roleid" if ($PSBoundParameters.Keys.Contains('startDateTime')) { $startDateTime = get-date ([datetime]::Parse($startDateTime)).touniversaltime() -f "yyyy-MM-ddTHH:mm:ssZ" } else { $startDateTime = get-date (get-date).touniversaltime() -f "yyyy-MM-ddTHH:mm:ssZ" #we get the date as UTC (remember to add a Z at the end or it will be translated to US timezone on import) } write-verbose "Calculated date time start is $startDateTime" # get role settings: $config = Get-PIMAzureResourcePolicy -tenantID $tenantID -scope $scope -rolename $rolename # if permanent assignement is requested check this is allowed in the rule if($permanent){ if( $config.AllowPermanentActiveAssignment -eq "false"){ throw "ERROR : The role $rolename does not allow permanent active assignement, exiting" } } # if Duration is not provided we will take the maxium value from the role setting if(!($PSBoundParameters.Keys.Contains('duration'))){ $duration = $config.MaximumActiveAssignmentDuration } write-verbose "assignement duration will be : $duration" if (!($PSBoundParameters.Keys.Contains('justification'))) { $justification = "Approved from EasyPIM module by $($(get-azcontext).account)" } $type = "AfterDuration" if ($permanent) { $type = "NoExpiration" } $body = ' { "properties": { "principalId": "'+ $principalID + '", "roleDefinitionId": "'+ $roleID + '", "requestType": "AdminAssign", "justification": "'+ $justification + '", "scheduleInfo": { "startDateTime": "'+ $startDateTime + '", "expiration": { "type": "'+ $type + '", "endDateTime": null, "duration": "'+ $duration + '" } } } ' $guid = New-Guid $restURI = "$armendpoint/roleAssignmentScheduleRequests/$($guid)?api-version=2020-10-01" write-verbose "sending PUT request at $restUri with body :`n $body" $response = Invoke-ARM -restURI $restUri -method PUT -body $body -Verbose:$false Write-Host "SUCCESS : Assignment created!" return $response } catch{Mycatch $_} } <# .Synopsis Create an eligible assignement at the provided scope .Description Eligible assignment require users to activate their role before using it. https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Parameter tenantID EntraID tenant ID .Parameter subscriptionID subscription ID .Parameter scope use scope parameter if you want to work at other scope than a subscription .Parameter principalID objectID of the principal (user, group or service principal) .Parameter rolename name of the role to assign .Parameter duration duration of the assignment, if not set we will use the maximum allowed value from the role policy .Parameter startDateTime When the assignment wil begin, if not set we will use current time .Parameter permanent Use this parameter if you want a permanent assignement (no expiration) .Parameter justification justification .Example PS> New-PIMAzureResourceEligibleAssigment -tenantID $tenantID -subscriptionID $subscriptionId -rolename "AcrPush" -principalID 3604fe63-cb67-4b60-99c9-707d46ab9092 -startDateTime "2/2/2024 18:20" Create an eligible assignment fot the role Arcpush, starting at a specific date and using default duration PS> New-PIMAzureResourceEligibleAssigment -tenantID $tenantID -subscriptionID $subscriptionId -rolename "webmaster" -principalID 3604fe63-cb67-4b60-99c9-707d46ab9092 -justification 'TEST' -permanent Create a permanent eligible assignement for the role webmaster .Link https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function New-PIMAzureResourceEligibleAssignment { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [String] # Entra ID tenantID $tenantID, [Parameter(Position = 1)] [String] # subscription ID $subscriptionID, [Parameter()] [String] # scope if not at the subscription level $scope, [Parameter(Mandatory = $true)] [String] # Principal ID $principalID, [Parameter(Mandatory = $true)] [string] # the rolename for which we want to create an assigment $rolename, [string] # duration of the assignment, if not set we will use the maximum allowed value from the role policy $duration, [string] # stat date of assignment if not provided we will use curent time $startDateTime, [string] # justification (will be auto generated if not provided) $justification, [switch] # the assignment will not expire $permanent ) try { if (!($PSBoundParameters.Keys.Contains('scope'))) { if (!($PSBoundParameters.Keys.Contains('subscriptionID'))) { throw "ERROR : You must provide a subsciption ID or a scope, exiting." } $scope = "/subscriptions/$subscriptionID" } $script:tenantID = $tenantID $ARMhost = "https://management.azure.com" $ARMendpoint = "$ARMhost/$scope/providers/Microsoft.Authorization" #1 get role id $restUri = "$ARMendpoint/roleDefinitions?api-version=2022-04-01&`$filter=roleName eq '$rolename'" $response = Invoke-ARM -restURI $restUri -method "get" -body $null $roleID = $response.value.id write-verbose "Getting role ID for $rolename at $restURI" write-verbose "role ID = $roleid" if ($PSBoundParameters.Keys.Contains('startDateTime')) { $startDateTime = get-date ([datetime]::Parse($startDateTime)).touniversaltime() -f "yyyy-MM-ddTHH:mm:ssZ" } else { $startDateTime = get-date (get-date).touniversaltime() -f "yyyy-MM-ddTHH:mm:ssZ" #we get the date as UTC (remember to add a Z at the end or it will be translated to US timezone on import) } write-verbose "Calculated date time start is $startDateTime" # get role settings: $config = Get-PIMAzureResourcePolicy -tenantID $tenantID -scope $scope -rolename $rolename # if permanent assignement is requested check this is allowed in the rule if ($permanent) { if ( $config.AllowPermanentEligibleAssignment -eq "false") { throw "ERROR : The role $rolename does not allow permanent eligible assignement, exiting" } } # if Duration is not provided we will take the maxium value from the role setting if (!($PSBoundParameters.Keys.Contains('duration'))) { $duration = $config.MaximumActiveAssignmentDuration } write-verbose "assignement duration will be : $duration" if (!($PSBoundParameters.Keys.Contains('justification'))) { $justification = "Approved from EasyPIM module by $($(get-azcontext).account)" } $type = "AfterDuration" if ($permanent) { $type = "NoExpiration" } $body = ' { "properties": { "principalId": "'+ $principalID + '", "roleDefinitionId": "'+ $roleID + '", "requestType": "AdminAssign", "justification": "'+ $justification + '", "scheduleInfo": { "startDateTime": "'+ $startDateTime + '", "expiration": { "type": "'+ $type + '", "endDateTime": null, "duration": "'+ $duration + '" } } } ' $guid = New-Guid $restURI = "$armendpoint/roleEligibilityScheduleRequests/$($guid)?api-version=2020-10-01" write-verbose "sending PUT request at $restUri with body :`n $body" $response = Invoke-ARM -restURI $restUri -method PUT -body $body -Verbose:$false Write-Host "SUCCESS : Assignment created!" return $response } catch { Mycatch $_ } } <# .Synopsis Create an active assignement for the role $rolename and for the principal $principalID .Description Active assignment does not require users to activate their role. https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Parameter tenantID EntraID tenant ID .Parameter principalID objectID of the principal (user, group or service principal) .Parameter rolename name of the role to assign .Parameter duration duration of the assignment, if not set we will use the maximum allowed value from the role policy .Parameter startDateTime When the assignment wil begin, if not set we will use current time .Parameter permanent Use this parameter if you want a permanent assignement (no expiration) .Parameter justification justification .Example PS> New-PIMEntraRoleActiveAssignment -tenantID $tenantID -subscriptionID $subscriptionId -rolename "AcrPush" -principalID 3604fe63-cb67-4b60-99c9-707d46ab9092 -startDateTime "2/2/2024 18:20" Create an active assignment fot the role Arcpush, starting at a specific date and using default duration PS> New-PIMEntraRoleActiveAssignment -tenantID $tenantID -subscriptionID $subscriptionId -rolename "webmaster" -principalID 3604fe63-cb67-4b60-99c9-707d46ab9092 -justification 'TEST' -permanent Create a permanent active assignement for the role webmaster .Link https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function New-PIMEntraRoleActiveAssignment { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [String] # Entra ID tenantID $tenantID, [Parameter(Mandatory = $true)] [String] # Principal ID $principalID, [Parameter(Mandatory = $true)] [string] # the rolename for which we want to create an assigment $rolename, [string] # duration of the assignment, if not set we will use the maximum allowed value from the role policy $duration, [string] # stat date of assignment if not provided we will use curent time $startDateTime, [string] # justification (will be auto generated if not provided) $justification, [switch] # the assignment will not expire $permanent ) try { $script:tenantID = $tenantID #1 check if the principal ID is a group, if yes confirm it is role-assignable $endpoint = "directoryObjects/$principalID" $response = invoke-graph -Endpoint $endpoint #$response if ($response."@odata.type" -eq "#microsoft.graph.group" -and $response.isAssignableToRole -ne "True") { throw "ERROR : The group $principalID is not role-assignable, exiting" } if ($PSBoundParameters.Keys.Contains('startDateTime')) { $startDateTime = get-date ([datetime]::Parse($startDateTime)).touniversaltime() -f "yyyy-MM-ddTHH:mm:ssZ" } else { $startDateTime = get-date (get-date).touniversaltime() -f "yyyy-MM-ddTHH:mm:ssZ" #we get the date as UTC (remember to add a Z at the end or it will be translated to US timezone on import) } write-verbose "Calculated date time start is $startDateTime" # 2 get role settings: $config = Get-PIMEntraRolePolicy -tenantID $tenantID -rolename $rolename #if permanent assignement is requested check this is allowed in the rule if ($permanent) { if ( $config.AllowPermanentActiveAssignment -eq "false") { throw "ERROR : The role $rolename does not allow permanent active assignement, exiting" } } # if Duration is not provided we will take the maxium value from the role setting if (!($PSBoundParameters.Keys.Contains('duration'))) { $duration = $config.MaximumActiveAssignmentDuration } write-verbose "assignement duration will be : $duration" if (!($PSBoundParameters.Keys.Contains('justification'))) { $justification = "Approved from EasyPIM module by $($(get-azcontext).account)" } $type = "AfterDuration" if ($permanent) { $type = "NoExpiration" } $body = ' { "action": "adminAssign", "justification": "'+ $justification + '", "roleDefinitionId": "'+ $config.roleID + '", "directoryScopeId": "/", "principalId": "'+ $principalID + '", "scheduleInfo": { "startDateTime": "'+ $startDateTime + '", "expiration": { "type": "'+ $type + '", "endDateTime": null, "duration": "'+ $duration + '" } }, "ticketInfo": { "ticketNumber": "EasyPIM", "ticketSystem": "EasyPIM" } } ' $endpoint = "roleManagement/directory/roleAssignmentScheduleRequests/" invoke-graph -Endpoint $endpoint -Method "POST" -body $body } catch { Mycatch $_ } } <# .Synopsis Create an eligible assignement for $rolename and for the principal $principalID .Description Eligible assignment require users to activate their role. https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Parameter tenantID EntraID tenant ID .Parameter principalID objectID of the principal (user, group or service principal) .Parameter rolename name of the role to assign .Parameter duration duration of the assignment, if not set we will use the maximum allowed value from the role policy .Parameter startDateTime When the assignment wil begin, if not set we will use current time .Parameter permanent Use this parameter if you want a permanent assignement (no expiration) .Parameter justification justification .Example PS> New-PIMEntraRoleEligibleAssignment -tenantID $tenantID -rolename "AcrPush" -principalID 3604fe63-cb67-4b60-99c9-707d46ab9092 -startDateTime "2/2/2024 18:20" Create an active assignment fot the role Arcpush, starting at a specific date and using default duration PS> New-PIMEntraRoleEligibleAssignment -tenantID $tenantID -rolename "webmaster" -principalID 3604fe63-cb67-4b60-99c9-707d46ab9092 -justification 'TEST' -permanent Create a permanent active assignement for the role webmaster .Link https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function New-PIMEntraRoleEligibleAssignment { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [String] # Entra ID tenantID $tenantID, [Parameter(Mandatory = $true)] [String] # Principal ID $principalID, [Parameter(Mandatory = $true)] [string] # the rolename for which we want to create an assigment $rolename, [string] # duration of the assignment, if not set we will use the maximum allowed value from the role policy $duration, [string] # stat date of assignment if not provided we will use curent time $startDateTime, [string] # justification (will be auto generated if not provided) $justification, [switch] # the assignment will not expire $permanent ) try { $script:tenantID = $tenantID #1 check if the principal ID is a group, if yes confirm it is role-assignable $endpoint = "directoryObjects/$principalID" $response = invoke-graph -Endpoint $endpoint #$response if ($response."@odata.type" -eq "#microsoft.graph.group" -and $response.isAssignableToRole -ne "True") { throw "ERROR : The group $principalID is not role-assignable, exiting" } if ($PSBoundParameters.Keys.Contains('startDateTime')) { $startDateTime = get-date ([datetime]::Parse($startDateTime)).touniversaltime().addseconds(30) -f "yyyy-MM-ddTHH:mm:ssZ" } else { $startDateTime = get-date (get-date).touniversaltime().addseconds(30) -f "yyyy-MM-ddTHH:mm:ssZ" #we get the date as UTC (remember to add a Z at the end or it will be translated to US timezone on import) } write-verbose "Calculated date time start is $startDateTime" # 2 get role settings: $config = Get-PIMEntraRolePolicy -tenantID $tenantID -rolename $rolename #if permanent assignement is requested check this is allowed in the rule if ($permanent) { if ( $config.AllowPermanentEligibleAssignment -eq "false") { throw "ERROR : The role $rolename does not allow permanent eligible assignement, exiting" } } # if Duration is not provided we will take the maxium value from the role setting if (!($PSBoundParameters.Keys.Contains('duration'))) { $duration = $config.MaximumEligibleAssignmentDuration } write-verbose "assignement duration will be : $duration" if (!($PSBoundParameters.Keys.Contains('justification'))) { $justification = "Approved from EasyPIM module by $($(get-azcontext).account)" } $type = "AfterDuration" #$type="afterDateTime" if ($permanent) { $type = "NoExpiration" } $body = ' { "action": "adminAssign", "justification": "'+ $justification + '", "roleDefinitionId": "'+ $config.roleID + '", "directoryScopeId": "/", "principalId": "'+ $principalID + '", "scheduleInfo": { "startDateTime": "'+ $startDateTime + '", "expiration": { "type": "'+ $type + '", "endDateTime": null, "duration": "'+ $duration + '" } } } ' $endpoint = "/roleManagement/directory/roleEligibilityScheduleRequests" write-verbose "patch body : $body" $null = invoke-graph -Endpoint $endpoint -Method "POST" -body $body } catch { MyCatch $_ } } <# .Synopsis Create an active assignement for the group $groupID and for the principal $principalID .Description Active assignment does not require users to activate their role. https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Parameter tenantID EntraID tenant ID .Parameter groupID objectID of the group .Parameter principalID objectID of the principal (user, group or service principal) .Parameter type member type (owner or member) .Parameter duration duration of the assignment, if not set we will use the maximum allowed value from the policy .Parameter startDateTime When the assignment wil begin, if not set we will use current time .Parameter permanent Use this parameter if you want a permanent assignement (no expiration) .Parameter justification justification .Example PS> New-PIMGroupActiveAssignment -tenantID $tenantID -groupID $gID -principalID $userID -type member -duration "P7D" Create an active assignment for the membership role of the group $gID and principal $userID starting now and using a duration of 7 days PS> New-PIMGroupActiveAssignment -tenantID $tenantID -groupID $gID -principalID $userID -type owner -permanent Create a permanent active assignement for the ownership role of the group $gID and principal $userID starting now .Link https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function New-PIMGroupActiveAssignment { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [String] # Entra ID tenantID $tenantID, [Parameter(Position = 1, Mandatory = $true)] [String] # Entra ID tenantID $groupID, [Parameter(Mandatory = $true)] [String] # Principal ID $principalID, [Parameter(Mandatory = $true)] [string] # the rolename for which we want to create an assigment $type, [string] # duration of the assignment, if not set we will use the maximum allowed value from the role policy $duration, [string] # stat date of assignment if not provided we will use curent time $startDateTime, [string] # justification (will be auto generated if not provided) $justification, [switch] # the assignment will not expire $permanent ) try { $script:tenantID = $tenantID if ($PSBoundParameters.Keys.Contains('startDateTime')) { $startDateTime = get-date ([datetime]::Parse($startDateTime)).touniversaltime().addseconds(30) -f "yyyy-MM-ddTHH:mm:ssZ" } else { $startDateTime = get-date (get-date).touniversaltime().addseconds(30) -f "yyyy-MM-ddTHH:mm:ssZ" #we get the date as UTC (remember to add a Z at the end or it will be translated to US timezone on import) } write-verbose "Calculated date time start is $startDateTime" # 2 get role settings: $config = Get-PIMgroupPolicy -tenantID $tenantID -groupID $groupID -type $type #if permanent assignement is requested check this is allowed in the rule if ($permanent) { if ( $config.AllowPermanentActiveAssignment -eq "false") { throw "ERROR : The role $rolename does not allow permanent eligible assignement, exiting" } } # if Duration is not provided we will take the maxium value from the role setting if (!($PSBoundParameters.Keys.Contains('duration'))) { $duration = $config.MaximumActiveAssignmentDuration } write-verbose "assignement duration will be : $duration" if (!($PSBoundParameters.Keys.Contains('justification'))) { $justification = "Approved from EasyPIM module by $($(get-azcontext).account)" } $exptype = "AfterDuration" #$type="afterDateTime" if ($permanent) { $exptype = "NoExpiration" } $body = ' { "action": "adminAssign", "accessID":"'+$type+'", "groupID":"'+$groupID+'", "justification": "'+ $justification + '", "principalId": "'+ $principalID + '", "scheduleInfo": { "startDateTime": "'+ $startDateTime + '", "expiration": { "type": "'+ $exptype + '", "duration": "'+ $duration + '" } } } ' $endpoint = "/identityGovernance/privilegedAccess/group/assignmentScheduleRequests" write-verbose "patch body : $body" $response = invoke-graph -Endpoint $endpoint -Method "POST" -body $body return $response } catch { MyCatch $_ } } <# .Synopsis Create an active assignement at the provided scope .Description Active assignment does not require users to activate their role. https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Parameter tenantID EntraID tenant ID .Parameter subscriptionID subscription ID .Parameter scope use scope parameter if you want to work at other scope than a subscription .Parameter principalID objectID of the principal (user, group or service principal) .Parameter rolename name of the role to assign .Parameter duration duration of the assignment, if not set we will use the maximum allowed value from the role policy .Parameter startDateTime When the assignment wil begin, if not set we will use current time .Parameter permanent Use this parameter if you want a permanent assignement (no expiration) .Parameter justification justification .Example PS> New-PIMEntraRoleEligibleAssignment -tenantID $tenantID -subscriptionID $subscriptionId -rolename "AcrPush" -principalID 3604fe63-cb67-4b60-99c9-707d46ab9092 -startDateTime "2/2/2024 18:20" Create an active assignment fot the role Arcpush, starting at a specific date and using default duration PS> New-PIMEntraRoleEligibleAssignment -tenantID $tenantID -subscriptionID $subscriptionId -rolename "webmaster" -principalID 3604fe63-cb67-4b60-99c9-707d46ab9092 -justification 'TEST' -permanent Create a permanent active assignement for the role webmaster .Link https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function New-PIMGroupEligibleAssignment { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [String] # Entra ID tenantID $tenantID, [Parameter(Position = 1, Mandatory = $true)] [String] # Entra ID tenantID $groupID, [Parameter(Mandatory = $true)] [String] # Principal ID $principalID, [Parameter(Mandatory = $true)] [string] # the rolename for which we want to create an assigment $type, [string] # duration of the assignment, if not set we will use the maximum allowed value from the role policy $duration, [string] # stat date of assignment if not provided we will use curent time $startDateTime, [string] # justification (will be auto generated if not provided) $justification, [switch] # the assignment will not expire $permanent ) try { $script:tenantID = $tenantID if ($PSBoundParameters.Keys.Contains('startDateTime')) { $startDateTime = get-date ([datetime]::Parse($startDateTime)).touniversaltime().addseconds(30) -f "yyyy-MM-ddTHH:mm:ssZ" } else { $startDateTime = get-date (get-date).touniversaltime().addseconds(30) -f "yyyy-MM-ddTHH:mm:ssZ" #we get the date as UTC (remember to add a Z at the end or it will be translated to US timezone on import) } write-verbose "Calculated date time start is $startDateTime" # 2 get role settings: $config = Get-PIMgroupPolicy -tenantID $tenantID -groupID $groupID -type $type #if permanent assignement is requested check this is allowed in the rule if ($permanent) { if ( $config.AllowPermanentEligibleAssignment -eq "false") { throw "ERROR : The role $rolename does not allow permanent eligble assignement, exiting" } } # if Duration is not provided we will take the maxium value from the role setting if (!($PSBoundParameters.Keys.Contains('duration'))) { $duration = $config.MaximumEligibleAssignmentDuration } write-verbose "assignement duration will be : $duration" if (!($PSBoundParameters.Keys.Contains('justification'))) { $justification = "Approved from EasyPIM module by $($(get-azcontext).account)" } $exptype = "AfterDuration" #$type="afterDateTime" if ($permanent) { $exptype = "NoExpiration" } $body = ' { "action": "adminAssign", "accessID":"'+$type+'", "groupID":"'+$groupID+'", "justification": "'+ $justification + '", "principalId": "'+ $principalID + '", "scheduleInfo": { "startDateTime": "'+ $startDateTime + '", "expiration": { "type": "'+ $exptype + '", "endDateTime": null, "duration": "'+ $duration + '" } } } ' $endpoint = "/identityGovernance/privilegedAccess/group/EligibilityScheduleRequests" write-verbose "patch body : $body" $response = invoke-graph -Endpoint $endpoint -Method "POST" -body $body Return $response } catch { MyCatch $_ } } <# .Synopsis Remove an active assignement at the provided scope .Description active assignment does not require users to activate their role before using it. https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Parameter tenantID EntraID tenant ID .Parameter subscriptionID subscription ID .Parameter scope use scope parameter if you want to work at other scope than a subscription .Parameter principalID objectID of the principal (user, group or service principal) .Parameter rolename name of the role to assign .Parameter justification justification .Example PS> Remove-PIMAzureResourceActiveAssigment -tenantID $tenantID -subscriptionID $subscriptionId -rolename "AcrPush" -principalID 3604fe63-cb67-4b60-99c9-707d46ab9092 Remove the active assignment for the role Arcpush and principal id 3604fe63-cb67-4b60-99c9-707d46ab9092 .Link https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Remove-PIMAzureResourceActiveAssignment { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [String] # Entra ID tenantID $tenantID, [Parameter(Position = 1)] [String] # subscription ID $subscriptionID, [Parameter()] [String] # scope if not at the subscription level $scope, [Parameter(Mandatory = $true)] [String] # Principal ID $principalID, [Parameter(Mandatory = $true)] [string] # the rolename for which we want to create an assigment $rolename, [string] # justification (will be auto generated if not provided) $justification ) try { if (!($PSBoundParameters.Keys.Contains('scope'))) { if (!($PSBoundParameters.Keys.Contains('subscriptionID'))) { throw "ERROR : You must provide a subsciption ID or a scope, exiting." } $scope = "/subscriptions/$subscriptionID" } $script:tenantID = $tenantID $ARMhost = "https://management.azure.com" $ARMendpoint = "$ARMhost/$scope/providers/Microsoft.Authorization" #1 get role id $restUri = "$ARMendpoint/roleDefinitions?api-version=2022-04-01&`$filter=roleName eq '$rolename'" $response = Invoke-ARM -restURI $restUri -method "get" -body $null $roleID = $response.value.id write-verbose "Getting role ID for $rolename at $restURI" write-verbose "role ID = $roleid" if ($PSBoundParameters.Keys.Contains('startDateTime')) { $startDateTime = get-date ([datetime]::Parse($startDateTime)).touniversaltime() -f "yyyy-MM-ddTHH:mm:ssZ" } else { $startDateTime = get-date (get-date).touniversaltime() -f "yyyy-MM-ddTHH:mm:ssZ" #we get the date as UTC (remember to add a Z at the end or it will be translated to US timezone on import) } write-verbose "Calculated date time start is $startDateTime" if (!($PSBoundParameters.Keys.Contains('justification'))) { $justification = "Removed from EasyPIM module by $($(get-azcontext).account)" } $type = "null" $body = ' { "properties": { "principalId": "'+ $principalID + '", "roleDefinitionId": "'+ $roleID + '", "requestType": "AdminRemove", "justification": "'+ $justification + '", "scheduleInfo": { "startDateTime": "'+ $startDateTime + '", "expiration": { "type": "'+ $type + '", "endDateTime": null, "duration": "'+ $duration + '" } } } ' $guid = New-Guid $restURI = "$armendpoint/roleAssignmentScheduleRequests/$($guid)?api-version=2020-10-01" write-verbose "sending PUT request at $restUri with body :`n $body" $response = Invoke-ARM -restURI $restUri -method PUT -body $body -Verbose:$false Write-Host "SUCCESS : Assignment removed!" return $response } catch { Mycatch $_ } } <# .Synopsis Remove an eligible assignement at the provided scope .Description Eligible assignment require users to activate their role before using it. https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Parameter tenantID EntraID tenant ID .Parameter subscriptionID subscription ID .Parameter scope use scope parameter if you want to work at other scope than a subscription .Parameter principalID objectID of the principal (user, group or service principal) .Parameter rolename name of the role to assign .Parameter justification justification .Example PS> Remove-PIMAzureResourceEligibleAssigment -tenantID $tenantID -subscriptionID $subscriptionId -rolename "AcrPush" -principalID 3604fe63-cb67-4b60-99c9-707d46ab9092 Remove the eligible assignment for the role Arcpush and principal id 3604fe63-cb67-4b60-99c9-707d46ab9092 .Link https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Remove-PIMAzureResourceEligibleAssignment { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [String] # Entra ID tenantID $tenantID, [Parameter(Position = 1)] [String] # subscription ID $subscriptionID, [Parameter()] [String] # scope if not at the subscription level $scope, [Parameter(Mandatory = $true)] [String] # Principal ID $principalID, [Parameter(Mandatory = $true)] [string] # the rolename for which we want to create an assigment $rolename, [string] # justification (will be auto generated if not provided) $justification ) try { if (!($PSBoundParameters.Keys.Contains('scope'))) { if (!($PSBoundParameters.Keys.Contains('subscriptionID'))) { throw "ERROR : You must provide a subsciption ID or a scope, exiting." } $scope = "/subscriptions/$subscriptionID" } $script:tenantID = $tenantID $ARMhost = "https://management.azure.com" $ARMendpoint = "$ARMhost/$scope/providers/Microsoft.Authorization" #1 check if there is a request for future assignment, in that case we need to cancel the request write-verbose "Checking if there is a future assignment for $principalID and $rolename at $scope" $response = get-pimazureResourceEligibleAssignment -tenantID $tenantID -scope $scope -includeFutureAssignments | Where-Object { $_.principalID -eq "$principalID" -and $_.rolename -eq "$rolename" } if ( !($null -eq $response) -and $response.status -ne "Provisioned" ) { #only non provisioned assignment can be canceled, else we need an admin remove Write-Verbose "Found a future assignment, we need to cancel it" $restURI = "$ARMendpoint/roleEligibilityScheduleRequests/$( $response.id.Split('/')[-1] )/cancel?api-version=2020-10-01" $response = invoke-arm -restURI $restURI -method POST -body $null Write-Host "SUCCESS : Future assignment canceled!" return $response } else { #1 get role id $restUri = "$ARMendpoint/roleDefinitions?api-version=2022-04-01&`$filter=roleName eq '$rolename'" $response = Invoke-ARM -restURI $restUri -method "get" -body $null $roleID = $response.value.id write-verbose "Getting role ID for $rolename at $restURI" write-verbose "role ID = $roleid" if ($PSBoundParameters.Keys.Contains('startDateTime')) { $startDateTime = get-date ([datetime]::Parse($startDateTime)).touniversaltime() -f "yyyy-MM-ddTHH:mm:ssZ" } else { $startDateTime = get-date (get-date).touniversaltime() -f "yyyy-MM-ddTHH:mm:ssZ" #we get the date as UTC (remember to add a Z at the end or it will be translated to US timezone on import) } write-verbose "Calculated date time start is $startDateTime" if (!($PSBoundParameters.Keys.Contains('justification'))) { $justification = "Removed from EasyPIM module by $($(get-azcontext).account)" } $type = "null" $body = ' { "properties": { "principalId": "'+ $principalID + '", "roleDefinitionId": "'+ $roleID + '", "requestType": "AdminRemove", "justification": "'+ $justification + '", "scheduleInfo": { "startDateTime": "'+ $startDateTime + '", "expiration": { "type": "'+ $type + '", "endDateTime": null, "duration": "'+ $duration + '" } } } ' $guid = New-Guid $restURI = "$armendpoint/roleEligibilityScheduleRequests/$($guid)?api-version=2020-10-01" write-verbose "sending PUT request at $restUri with body :`n $body" $response = Invoke-ARM -restURI $restUri -method PUT -body $body -Verbose:$false Write-Host "SUCCESS : Assignment removed!" return $response } } catch { MyCatch $_ } } <# .Synopsis Remove an active assignement for $rolename and for the principal $principalID .Description Active assignment does not require users to activate their role. https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Parameter tenantID EntraID tenant ID .Parameter subscriptionID subscription ID .Parameter scope use scope parameter if you want to work at other scope than a subscription .Parameter principalID objectID of the principal (user, group or service principal) .Parameter rolename name of the role to assign .Parameter duration duration of the assignment, if not set we will use the maximum allowed value from the role policy .Parameter startDateTime When the assignment wil begin, if not set we will use current time .Parameter permanent Use this parameter if you want a permanent assignement (no expiration) .Parameter justification justification .Example PS> Remove-PIMEntraRoleActiveAssignment -tenantID $tenantID -rolename "AcrPush" -principalID 3604fe63-cb67-4b60-99c9-707d46ab9092 -startDateTime "2/2/2024 18:20" Remove the active assignment for the role Arcpush and principal $principalID, at a specific date PS> Remove-PIMEntraRoleActiveAssignment -tenantID $tenantID -rolename "webmaster" -principalname "loic" -justification 'TEST' Remove the active assignement for the role webmaster and username "loic" .Link https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Remove-PIMEntraRoleActiveAssignment { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [String] # Entra ID tenantID $tenantID, [Parameter(Mandatory = $true)] [String] # Principal ID $principalID, [Parameter(Mandatory = $true)] [string] # the rolename for which we want to create an assigment $rolename, [string] # stat date of assignment if not provided we will use curent time $startDateTime, [string] # justification (will be auto generated if not provided) $justification ) try { $script:tenantID = $tenantID if ($PSBoundParameters.Keys.Contains('startDateTime')) { $startDateTime = get-date ([datetime]::Parse($startDateTime)).touniversaltime().addseconds(30) -f "yyyy-MM-ddTHH:mm:ssZ" } else { $startDateTime = get-date (get-date).touniversaltime().addseconds(30) -f "yyyy-MM-ddTHH:mm:ssZ" #we get the date as UTC (remember to add a Z at the end or it will be translated to US timezone on import) } write-verbose "Calculated date time start is $startDateTime" # 2 get role settings: $config = Get-PIMEntraRolePolicy -tenantID $tenantID -rolename $rolename #if permanent assignement is requested check this is allowed in the rule if ($permanent) { if ( $config.AllowPermanentEligibleAssignment -eq "false") { throw "ERROR : The role $rolename does not allow permanent eligible assignement, exiting" } } # if Duration is not provided we will take the maxium value from the role setting if (!($PSBoundParameters.Keys.Contains('duration'))) { $duration = $config.MaximumEligibleAssignmentDuration } write-verbose "assignement duration will be : $duration" if (!($PSBoundParameters.Keys.Contains('justification'))) { $justification = "Approved from EasyPIM module by $($(get-azcontext).account)" } $type = "AfterDuration" #$type="afterDateTime" if ($permanent) { $type = "NoExpiration" } $body = ' { "action": "adminRemove", "justification": "'+ $justification + '", "roleDefinitionId": "'+ $config.roleID + '", "directoryScopeId": "/", "principalId": "'+ $principalID + '", "scheduleInfo": { "startDateTime": "'+ $startDateTime + '", "expiration": { "type": "'+ $type + '", "endDateTime": null, "duration": "'+ $duration + '" } } } ' $endpoint = "/roleManagement/directory/roleAssignmentScheduleRequests" write-verbose "patch body : $body" $null = invoke-graph -Endpoint $endpoint -Method "POST" -body $body Write-Host "SUCCESS : Assignment removed!" } catch { Mycatch $_ } } <# .Synopsis Create an active assignement at the provided scope .Description Active assignment does not require users to activate their role. https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Parameter tenantID EntraID tenant ID .Parameter subscriptionID subscription ID .Parameter scope use scope parameter if you want to work at other scope than a subscription .Parameter principalID objectID of the principal (user, group or service principal) .Parameter rolename name of the role to assign .Parameter duration duration of the assignment, if not set we will use the maximum allowed value from the role policy .Parameter startDateTime When the assignment wil begin, if not set we will use current time .Parameter permanent Use this parameter if you want a permanent assignement (no expiration) .Parameter justification justification .Example PS> New-PIMEntraRoleEligibleAssignment -tenantID $tenantID -subscriptionID $subscriptionId -rolename "AcrPush" -principalID 3604fe63-cb67-4b60-99c9-707d46ab9092 -startDateTime "2/2/2024 18:20" Create an active assignment fot the role Arcpush, starting at a specific date and using default duration PS> New-PIMEntraRoleEligibleAssignment -tenantID $tenantID -subscriptionID $subscriptionId -rolename "webmaster" -principalID 3604fe63-cb67-4b60-99c9-707d46ab9092 -justification 'TEST' -permanent Create a permanent active assignement for the role webmaster .Link https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Remove-PIMEntraRoleEligibleAssignment { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [String] # Entra ID tenantID $tenantID, [Parameter(Mandatory = $true)] [String] # Principal ID $principalID, [Parameter(Mandatory = $true)] [string] # the rolename for which we want to create an assigment $rolename, [string] # duration of the assignment, if not set we will use the maximum allowed value from the role policy $duration, [string] # stat date of assignment if not provided we will use curent time $startDateTime, [string] # justification (will be auto generated if not provided) $justification, [switch] # the assignment will not expire $permanent ) try { $script:tenantID = $tenantID if ($PSBoundParameters.Keys.Contains('startDateTime')) { $startDateTime = get-date ([datetime]::Parse($startDateTime)).touniversaltime().addseconds(30) -f "yyyy-MM-ddTHH:mm:ssZ" } else { $startDateTime = get-date (get-date).touniversaltime().addseconds(30) -f "yyyy-MM-ddTHH:mm:ssZ" #we get the date as UTC (remember to add a Z at the end or it will be translated to US timezone on import) } write-verbose "Calculated date time start is $startDateTime" # 2 get role settings: $config = Get-PIMEntraRolePolicy -tenantID $tenantID -rolename $rolename #if permanent assignement is requested check this is allowed in the rule if ($permanent) { if ( $config.AllowPermanentEligibleAssignment -eq "false") { throw "ERROR : The role $rolename does not allow permanent eligible assignement, exiting" } } # if Duration is not provided we will take the maxium value from the role setting if (!($PSBoundParameters.Keys.Contains('duration'))) { $duration = $config.MaximumEligibleAssignmentDuration } write-verbose "assignement duration will be : $duration" if (!($PSBoundParameters.Keys.Contains('justification'))) { $justification = "Approved from EasyPIM module by $($(get-azcontext).account)" } $type = "AfterDuration" #$type="afterDateTime" if ($permanent) { $type = "NoExpiration" } $body = ' { "action": "adminRemove", "justification": "'+ $justification + '", "roleDefinitionId": "'+ $config.roleID + '", "directoryScopeId": "/", "principalId": "'+ $principalID + '", "scheduleInfo": { "startDateTime": "'+ $startDateTime + '", "expiration": { "type": "'+ $type + '", "endDateTime": null, "duration": "'+ $duration + '" } } } ' $endpoint = "/roleManagement/directory/roleEligibilityScheduleRequests" write-verbose "patch body : $body" $null = invoke-graph -Endpoint $endpoint -Method "POST" -body $body Write-Host "SUCCESS : Assignment removed!" } catch { Mycatch $_ } } <# .Synopsis Remove an active assignement at the provided scope .Description Active assignment does not require users to activate their role. https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Parameter tenantID EntraID tenant ID .Parameter principalID objectID of the principal (user, group or service principal) .Parameter groupID ID of the group .Parameter type member or owner .Parameter startDateTime When the assignment wil begin, if not set we will use current time .Parameter permanent Use this parameter if you want a permanent assignement (no expiration) .Parameter justification justification .Example PS> New-PIMEntraRoleEligibleAssignment -tenantID $tenantID -subscriptionID $subscriptionId -rolename "AcrPush" -principalID 3604fe63-cb67-4b60-99c9-707d46ab9092 -startDateTime "2/2/2024 18:20" Create an active assignment fot the role Arcpush, starting at a specific date and using default duration PS> New-PIMEntraRoleEligibleAssignment -tenantID $tenantID -subscriptionID $subscriptionId -rolename "webmaster" -principalID 3604fe63-cb67-4b60-99c9-707d46ab9092 -justification 'TEST' -permanent Create a permanent active assignement for the role webmaster .Link https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Remove-PIMGroupActiveAssignment { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [String] # Entra ID tenantID $tenantID, [Parameter(Mandatory = $true)] [String] # Principal ID $principalID, [Parameter(Mandatory = $true)] [string] # the group ID $groupID, [Parameter(Mandatory = $true)] [string] # member or owner $type, [string] # duration of the assignment, if not set we will use the maximum allowed value from the role policy $duration, [string] # stat date of assignment if not provided we will use curent time $startDateTime, [string] # justification (will be auto generated if not provided) $justification, [switch] # the assignment will not expire $permanent ) try { $script:tenantID = $tenantID if ($PSBoundParameters.Keys.Contains('startDateTime')) { $startDateTime = get-date ([datetime]::Parse($startDateTime)).touniversaltime().addseconds(30) -f "yyyy-MM-ddTHH:mm:ssZ" } else { $startDateTime = get-date (get-date).touniversaltime().addseconds(30) -f "yyyy-MM-ddTHH:mm:ssZ" #we get the date as UTC (remember to add a Z at the end or it will be translated to US timezone on import) } write-verbose "Calculated date time start is $startDateTime" # 2 get role settings: $config = Get-PIMGroupPolicy -tenantID $tenantID -groupID $groupid -type $type #if permanent assignement is requested check this is allowed in the rule if ($permanent) { if ( $config.AllowPermanentActiveAssignment -eq "false") { throw "ERROR : The role $rolename does not allow permanent eligible assignement, exiting" } } # if Duration is not provided we will take the maxium value from the role setting if (!($PSBoundParameters.Keys.Contains('duration'))) { $duration = $config.MaximumActiveAssignmentDuration } write-verbose "assignement duration will be : $duration" if (!($PSBoundParameters.Keys.Contains('justification'))) { $justification = "Approved from EasyPIM module by $($(get-azcontext).account)" } $exptype = "AfterDuration" #$type="afterDateTime" if ($permanent) { $exptype = "NoExpiration" } $body = ' { "action": "adminRemove", "accessID":"'+$type+'", "groupID":"'+$groupID+'", "justification": "'+ $justification + '", "principalId": "'+ $principalID + '", "scheduleInfo": { "startDateTime": "'+ $startDateTime + '", "expiration": { "type": "'+ $exptype + '", "duration": "'+ $duration + '" } } } ' $endpoint = "/identityGovernance/privilegedAccess/group/assignmentScheduleRequests" write-verbose "patch body : $body" $null = invoke-graph -Endpoint $endpoint -Method "POST" -body $body Write-Host "SUCCESS : Assignment removed!" } catch { Mycatch $_ } } <# .Synopsis Create an active assignement at the provided scope .Description Active assignment does not require users to activate their role. https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Parameter tenantID EntraID tenant ID .Parameter subscriptionID subscription ID .Parameter scope use scope parameter if you want to work at other scope than a subscription .Parameter principalID objectID of the principal (user, group or service principal) .Parameter rolename name of the role to assign .Parameter duration duration of the assignment, if not set we will use the maximum allowed value from the role policy .Parameter startDateTime When the assignment wil begin, if not set we will use current time .Parameter permanent Use this parameter if you want a permanent assignement (no expiration) .Parameter justification justification .Example PS> New-PIMEntraRoleEligibleAssignment -tenantID $tenantID -subscriptionID $subscriptionId -rolename "AcrPush" -principalID 3604fe63-cb67-4b60-99c9-707d46ab9092 -startDateTime "2/2/2024 18:20" Create an active assignment fot the role Arcpush, starting at a specific date and using default duration PS> New-PIMEntraRoleEligibleAssignment -tenantID $tenantID -subscriptionID $subscriptionId -rolename "webmaster" -principalID 3604fe63-cb67-4b60-99c9-707d46ab9092 -justification 'TEST' -permanent Create a permanent active assignement for the role webmaster .Link https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Remove-PIMGroupEligibleAssignment { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")] [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $true)] [String] # Entra ID tenantID $tenantID, [Parameter(Mandatory = $true)] [String] # Principal ID $principalID, [Parameter(Mandatory = $true)] [string] # the group ID $groupID, [Parameter(Mandatory = $true)] [string] # member or owner $type, [string] # duration of the assignment, if not set we will use the maximum allowed value from the role policy $duration, [string] # stat date of assignment if not provided we will use curent time $startDateTime, [string] # justification (will be auto generated if not provided) $justification, [switch] # the assignment will not expire $permanent ) try { $script:tenantID = $tenantID if ($PSBoundParameters.Keys.Contains('startDateTime')) { $startDateTime = get-date ([datetime]::Parse($startDateTime)).touniversaltime().addseconds(30) -f "yyyy-MM-ddTHH:mm:ssZ" } else { $startDateTime = get-date (get-date).touniversaltime().addseconds(30) -f "yyyy-MM-ddTHH:mm:ssZ" #we get the date as UTC (remember to add a Z at the end or it will be translated to US timezone on import) } write-verbose "Calculated date time start is $startDateTime" # 2 get role settings: $config = Get-PIMGroupPolicy -tenantID $tenantID -groupID $groupid -type $type #if permanent assignement is requested check this is allowed in the rule if ($permanent) { if ( $config.AllowPermanentEligibleAssignment -eq "false") { throw "ERROR : The role $rolename does not allow permanent eligible assignement, exiting" } } # if Duration is not provided we will take the maxium value from the role setting if (!($PSBoundParameters.Keys.Contains('duration'))) { $duration = $config.MaximumEligibleAssignmentDuration } write-verbose "assignement duration will be : $duration" if (!($PSBoundParameters.Keys.Contains('justification'))) { $justification = "Approved from EasyPIM module by $($(get-azcontext).account)" } $exptype = "AfterDuration" #$type="afterDateTime" if ($permanent) { $exptype = "NoExpiration" } $body = ' { "action": "adminRemove", "accessID":"'+$type+'", "groupID":"'+$groupID+'", "justification": "'+ $justification + '", "principalId": "'+ $principalID + '", "scheduleInfo": { "startDateTime": "'+ $startDateTime + '", "expiration": { "type": "'+ $exptype + '", "duration": "'+ $duration + '" } } } ' $endpoint = "/identityGovernance/privilegedAccess/group/eligibilityScheduleRequests" write-verbose "patch body : $body" $null = invoke-graph -Endpoint $endpoint -Method "POST" -body $body Write-Host "SUCCESS : Assignment removed!" } catch { Mycatch $_ } } <# .Synopsis Set the setting of the role $rolename at the subscription scope where subscription = $subscription .Description Set the setting of the role $rolename at the subscription scope where subscription = $subscription .Example PS> Set-PIMAzureResourcePolicy -tenantID $tenantID -subscriptionID $subscriptionID -rolename webmaster -ActivationDuration "PT8H" Limit the maximum PIM activation duration to 8h .EXAMPLE PS> Set-PIMAzureResourcePolicy -TenantID $tenantID -SubscriptionId $subscriptionID -rolename "contributor" -Approvers @(@{"Id"="00b34bb3-8a6b-45ce-a7bb-c7f7fb400507";"Name"="John";"Type"="user"}) -ApprovalRequired $true Require activation approval and set John as an approver .Link .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Set-PIMAzureResourcePolicy { [CmdletBinding(DefaultParameterSetName='Default',SupportsShouldProcess = $true)] [OutputType([bool])] param ( [Parameter(Position = 0, Mandatory = $true)] [System.String] # Tenant ID $tenantID, [Parameter(ParameterSetName = 'Default',Position = 1, Mandatory = $true)] [System.String] #subscriptionID $subscriptionID, [Parameter(ParameterSetName = 'Scope',Position = 1, Mandatory = $true)] [System.String] #scope $scope, [Parameter(Position = 2, Mandatory = $true)] [System.String[]] #list of role to update $rolename, [System.String] # Maximum activation duration $ActivationDuration = $null, [Parameter(HelpMessage = "Accepted values: 'None' or any combination of these options (Case SENSITIVE): 'Justification, 'MultiFactorAuthentication', 'Ticketing'")] [ValidateScript({ # accepted values: "None","Justification", "MultiFactorAuthentication", "Ticketing" # WARNING: options are CASE SENSITIVE $script:valid = $true $acceptedValues = @("None", "Justification", "MultiFactorAuthentication", "Ticketing") $_ | ForEach-Object { if (!( $acceptedValues -Ccontains $_)) { $script:valid = $false } } return $script:valid })] [System.String[]] # Activation requirement $ActivationRequirement, [Parameter(HelpMessage = "Accepted values: 'None' or any combination of these options (Case SENSITIVE): 'Justification, 'MultiFactorAuthentication'")] [ValidateScript({ # accepted values: "None","Justification", "MultiFactorAuthentication" # WARNING: options are CASE SENSITIVE $script:valid = $true $acceptedValues = @("None", "Justification", "MultiFactorAuthentication") $_ | ForEach-Object { if (!( $acceptedValues -Ccontains $_)) { $script:valid = $false } } return $script:valid })] [System.String[]] # Active Assignation requirement $ActiveAssignationRequirement, [Parameter()] [Bool] # Is authentication context required? ($true|$false) $AuthenticationContext_Enabled, [Parameter()] [String] # Authentication context value? (ex c1) $AuthenticationContext_Value, [Parameter()] [Bool] # Is approval required to activate a role? ($true|$false) $ApprovalRequired, [Parameter()] # Array of approvers in the format: @(@{"Id"=<ObjectID>;"Name"="John":"Type"="user|group"}, .... ) $Approvers, [Parameter()] [System.String] # Maximum Eligility Duration $MaximumEligibilityDuration = $null, [Parameter()] [Bool] # Allow permanent eligibility? ($true|$false) $AllowPermanentEligibility, [Parameter()] [System.String] # Maximum active assignment duration # Duration ref https://en.wikipedia.org/wiki/ISO_8601#Durations $MaximumActiveAssignmentDuration = $null, [Parameter()] [Bool] # Allow permanent active assignement? ($true|$false) $AllowPermanentActiveAssignment, [Parameter()] [System.Collections.Hashtable] # Admin Notification when eligible role is assigned # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_EligibleAssignment_Alert, [Parameter()] [System.Collections.Hashtable] # End user notification when eligible role is assigned # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_EligibleAssignment_Assignee, [Parameter()] [System.Collections.Hashtable] # Approver notification when eligible role is assigned # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_EligibleAssignment_Approver, [Parameter()] [System.Collections.Hashtable] # Admin Notification when an active role is assigned # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_ActiveAssignment_Alert, [Parameter()] [System.Collections.Hashtable] # End user Notification when an active role is assigned # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_ActiveAssignment_Assignee, [Parameter()] [System.Collections.Hashtable] # Approver Notification when an active role is assigned # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_ActiveAssignment_Approver, [Parameter()] [System.Collections.Hashtable] # Admin Notification when a is activated # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_Activation_Alert, [Parameter()] [System.Collections.Hashtable] # End user Notification when a role is activated # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_Activation_Assignee, [Parameter()] [System.Collections.Hashtable] # Approvers Notification when a role is activated # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_Activation_Approver ) try { $p = @() $PSBoundParameters.Keys | ForEach-Object { $p += "$_ =>" + $PSBoundParameters[$_] } $p = $p -join ', ' Write-Verbose "Function Set-PIMAzureResourcePolicy is starting with parameters: $p" $script:subscriptionID = $subscriptionID if (!($PSBoundParameters.Keys.Contains('scope'))) { $script:scope = "subscriptions/$script:subscriptionID" } else { $script:scope = $scope } write-verbose "scope: $script:scope" $script:tenantID=$tenantID #at least one approver required if approval is enable # todo chech if a parameterset would be better if ($ApprovalRequired -eq $true -and $null -eq $Approvers ) { throw "`n /!\ At least one approver is required if approval is enable, please set -Approvers parameter`n`n" } $rolename | ForEach-Object { $config = get-config $script:scope $_ $rules = @() if ($PSBoundParameters.Keys.Contains('ActivationDuration')) { $rules += Set-ActivationDuration $ActivationDuration } if ($PSBoundParameters.Keys.Contains('ActivationRequirement')) { $rules += Set-ActivationRequirement $ActivationRequirement } if ($PSBoundParameters.Keys.Contains('ActiveAssignationRequirement')) { $rules += Set-ActiveAssignmentRequirement $ActiveAssignationRequirement } if ($PSBoundParameters.Keys.Contains('AuthenticationContext_Enabled')) { if (!($PSBoundParameters.Keys.Contains('AuthenticationContext_Value'))) { $AuthenticationContext_Value = $null } $rules += Set-AuthenticationContext $AuthenticationContext_Enabled $AuthenticationContext_Value } # Approval and approvers if ( ($PSBoundParameters.Keys.Contains('ApprovalRequired')) -or ($PSBoundParameters.Keys.Contains('Approvers'))) { $rules += Set-Approval $ApprovalRequired $Approvers } # eligibility assignement if ( $PSBoundParameters.ContainsKey('MaximumEligibilityDuration') -or ( $PSBoundParameters.ContainsKey('AllowPermanentEligibility'))) { #if values are not set, use the ones from the curent config write-verbose "Maximum Eligibiliy duration from curent config: $($config.MaximumEligibleAssignmentDuration)" if (!( $PSBoundParameters.ContainsKey('MaximumEligibilityDuration'))) { $MaximumEligibilityDuration = $config.MaximumEligibleAssignmentDuration } if (!( $PSBoundParameters.ContainsKey('AllowPermanentEligibility'))) { $AllowPermanentEligibility = $config.AllowPermanentEligibleAssignment } if ( ($false -eq $AllowPermanentEligibility) -and ( ($MaximumEligibilityDuration -eq "") -or ($null -eq $MaximumEligibilityDuration) )){ throw "ERROR: you requested the assignement to expire but the maximum duration is not defined, please use the MaximumEligibilityDuration parameter" } $rules += Set-EligibilityAssignment $MaximumEligibilityDuration $AllowPermanentEligibility } #active assignement limits if ( $PSBoundParameters.ContainsKey('MaximumActiveAssignmentDuration') -or ( $PSBoundParameters.ContainsKey('AllowPermanentActiveAssignment'))) { #if values are not set, use the ones from the curent config write-verbose "Maximum Active duration from curent config: $($config.MaximumActiveAssignmentDuration)" if (!( $PSBoundParameters.ContainsKey('MaximumActiveAssignmentDuration'))) { $MaximumActiveAssignmentDuration = $config.MaximumActiveAssignmentDuration } if (!( $PSBoundParameters.ContainsKey('AllowPermanentActiveAssignment'))) { $AllowPermanentActiveAssignment = $config.AllowPermanentActiveAssignment } if ( ($false -eq $AllowPermanentActiveAssignment) -and ( ($MaximumActiveAssignmentDuration -eq "") -or ($null -eq $MaximumActiveAssignmentDuration) )){ throw "ERROR: you requested the assignement to expire but the maximum duration is not defined, please use the MaximumActiveAssignmentDuration parameter" } $rules += Set-ActiveAssignment $MaximumActiveAssignmentDuration $AllowPermanentActiveAssignment } ################# # Notifications # ################# # Notif Eligibility assignment Alert if ($PSBoundParameters.Keys.Contains('Notification_EligibleAssignment_Alert')) { $rules += Set-Notification_EligibleAssignment_Alert $Notification_EligibleAssignment_Alert } # Notif elligibility assignee if ($PSBoundParameters.Keys.Contains('Notification_EligibleAssignment_Assignee')) { $rules += Set-Notification_EligibleAssignment_Assignee $Notification_EligibleAssignment_Assignee } # Notif elligibility approver if ($PSBoundParameters.Keys.Contains('Notification_EligibleAssignment_Approver')) { $rules += Set-Notification_EligibleAssignment_Approver $Notification_EligibleAssignment_Approver } # Notif Active Assignment Alert if ($PSBoundParameters.Keys.Contains('Notification_ActiveAssignment_Alert')) { $rules += Set-Notification_ActiveAssignment_Alert $Notification_ActiveAssignment_Alert } # Notif Active Assignment Assignee if ($PSBoundParameters.Keys.Contains('Notification_ActiveAssignment_Assignee')) { $rules += Set-Notification_ActiveAssignment_Assignee $Notification_ActiveAssignment_Assignee } # Notif Active Assignment Approvers if ($PSBoundParameters.Keys.Contains('Notification_ActiveAssignment_Approver')) { $rules += Set-Notification_ActiveAssignment_Approver $Notification_ActiveAssignment_Approver } # Notification Activation alert if ($PSBoundParameters.Keys.Contains('Notification_Activation_Alert')) { $rules += Set-Notification_Activation_Alert $Notification_Activation_Alert } # Notification Activation Assignee if ($PSBoundParameters.Keys.Contains('Notification_Activation_Assignee')) { $rules += Set-Notification_Activation_Assignee $Notification_Activation_Assignee } # Notification Activation Approvers if ($PSBoundParameters.Keys.Contains('Notification_Activation_Approver')) { $rules += Set-Notification_Activation_Approver $Notification_Activation_Approver } # Bringing all the rules together and patch the policy $allrules = $rules -join ',' Write-Verbose "All rules: $allrules" #Patching the policy if ($PSCmdlet.ShouldProcess($_, "Udpdating policy")) { $null = Update-Policy $config.policyID $allrules } } log "Success, policy updated" return } catch { MyCatch $_ } } <# .Synopsis Set the setting of the role $rolename .Description Set the setting of the role $rolename .Example PS> Set-PIMEntraRolePolicy -tenantID $tenantID -rolename webmaster -ActivationDuration "PT8H" Limit the maximum PIM activation duration to 8h .EXAMPLE PS> Set-PIMEntraRolePolicy -TenantID $tenantID -rolename "contributor" -Approvers @(@{"Id"="00b34bb3-8a6b-45ce-a7bb-c7f7fb400507";"Name"="John";"Type"="user"}) -ApprovalRequired $true Require activation approval and set John as an approver .Link .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Set-PIMEntraRolePolicy { [CmdletBinding(DefaultParameterSetName='Default',SupportsShouldProcess = $true)] [OutputType([bool])] param ( [Parameter(Position = 0, Mandatory = $true)] [System.String] # Tenant ID $tenantID, [Parameter(Position = 1, Mandatory = $true)] [System.String[]] #list of role to update $rolename, [System.String] # Maximum activation duration $ActivationDuration = $null, [Parameter(HelpMessage = "Accepted values: 'None' or any combination of these options (Case SENSITIVE): 'Justification, 'MultiFactorAuthentication', 'Ticketing'")] [ValidateScript({ # accepted values: "None","Justification", "MultiFactorAuthentication", "Ticketing" # WARNING: options are CASE SENSITIVE $script:valid = $true $acceptedValues = @("None", "Justification", "MultiFactorAuthentication", "Ticketing") $_ | ForEach-Object { if (!( $acceptedValues -Ccontains $_)) { $script:valid = $false } } return $script:valid })] [System.String[]] # Activation requirement $ActivationRequirement, [Parameter(HelpMessage = "Accepted values: 'None' or any combination of these options (Case SENSITIVE): 'Justification, 'MultiFactorAuthentication'")] [ValidateScript({ # accepted values: "None","Justification", "MultiFactorAuthentication" # WARNING: options are CASE SENSITIVE $script:valid = $true $acceptedValues = @("None", "Justification", "MultiFactorAuthentication") $_ | ForEach-Object { if (!( $acceptedValues -Ccontains $_)) { $script:valid = $false } } return $script:valid })] [System.String[]] # Active assignment requirement $ActiveAssignmentRequirement, [Parameter()] [Bool] # Is authentication context required? ($true|$false) $AuthenticationContext_Enabled, [Parameter()] [String] # Authentication context value? (ex c1) $AuthenticationContext_Value, [Parameter()] [Bool] # Is approval required to activate a role? ($true|$false) $ApprovalRequired, [Parameter()] # Array of approvers in the format: @(@{"Id"=<ObjectID>;"Name"="John":"Type"="user|group"}, .... ) $Approvers, [Parameter()] [System.String] # Maximum Eligility Duration $MaximumEligibilityDuration = $null, [Parameter()] [Bool] # Allow permanent eligibility? ($true|$false) $AllowPermanentEligibility, [Parameter()] [System.String] # Maximum active assignment duration # Duration ref https://en.wikipedia.org/wiki/ISO_8601#Durations $MaximumActiveAssignmentDuration = $null, [Parameter()] [Bool] # Allow permanent active assignement? ($true|$false) $AllowPermanentActiveAssignment, [Parameter()] [System.Collections.Hashtable] # Admin Notification when eligible role is assigned # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_EligibleAssignment_Alert, [Parameter()] [System.Collections.Hashtable] # End user notification when eligible role is assigned # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_EligibleAssignment_Assignee, [Parameter()] [System.Collections.Hashtable] # Approver notification when eligible role is assigned # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_EligibleAssignment_Approver, [Parameter()] [System.Collections.Hashtable] # Admin Notification when an active role is assigned # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_ActiveAssignment_Alert, [Parameter()] [System.Collections.Hashtable] # End user Notification when an active role is assigned # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_ActiveAssignment_Assignee, [Parameter()] [System.Collections.Hashtable] # Approver Notification when an active role is assigned # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_ActiveAssignment_Approver, [Parameter()] [System.Collections.Hashtable] # Admin Notification when a is activated # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_Activation_Alert, [Parameter()] [System.Collections.Hashtable] # End user Notification when a role is activated # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_Activation_Assignee, [Parameter()] [System.Collections.Hashtable] # Approvers Notification when a role is activated # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_Activation_Approver ) try { $p = @() $PSBoundParameters.Keys | ForEach-Object { $p += "$_ =>" + $PSBoundParameters[$_] } $p = $p -join ', ' write-verbose "Function Set-PIMEntraRolePolicy is starting with parameters: $p" $script:tenantID=$tenantID #at least one approver required if approval is enable # todo chech if a parameterset would be better if ($ApprovalRequired -eq $true -and $null -eq $Approvers ) { throw "`n /!\ At least one approver is required if approval is enable, please set -Approvers parameter`n`n" } $rolename | ForEach-Object { $script:config = get-EntraRoleconfig $_ $rules = @() if ($PSBoundParameters.Keys.Contains('ActivationDuration')) { $rules += Set-ActivationDuration $ActivationDuration -EntraRole } if ($PSBoundParameters.Keys.Contains('ActivationRequirement')) { $rules += Set-ActivationRequirement $ActivationRequirement -EntraRole } if ($PSBoundParameters.Keys.Contains('ActiveAssignmentRequirement')) { $rules += Set-ActiveAssignmentRequirement $ActiveAssignmentRequirement -EntraRole } if ($PSBoundParameters.Keys.Contains('AuthenticationContext_Enabled')) { if (!($PSBoundParameters.Keys.Contains('AuthenticationContext_Value'))) { $AuthenticationContext_Value = $null } $rules += Set-AuthenticationContext $AuthenticationContext_Enabled $AuthenticationContext_Value -entraRole } # Approval and approvers if ( ($PSBoundParameters.Keys.Contains('ApprovalRequired')) -or ($PSBoundParameters.Keys.Contains('Approvers'))) { $rules += Set-Approval $ApprovalRequired $Approvers -EntraRole } # eligibility assignement if ( $PSBoundParameters.ContainsKey('MaximumEligibilityDuration') -or ( $PSBoundParameters.ContainsKey('AllowPermanentEligibility'))) { #if values are not set, use the ones from the curent config write-verbose "Maximum Eligibiliy duration from curent config: $($script:config.MaximumEligibleAssignmentDuration)" if (!( $PSBoundParameters.ContainsKey('MaximumEligibilityDuration'))) { $MaximumEligibilityDuration = $script:config.MaximumEligibleAssignmentDuration } if (!( $PSBoundParameters.ContainsKey('AllowPermanentEligibility'))) { $AllowPermanentEligibility = $script:config.AllowPermanentEligibleAssignment } if ( ($false -eq $AllowPermanentEligibility) -and ( ($MaximumEligibilityDuration -eq "") -or ($null -eq $MaximumEligibilityDuration) )){ throw "ERROR: you requested the assignement to expire but the maximum duration is not defined, please use the MaximumEligibilityDuration parameter" } $rules += Set-EligibilityAssignment $MaximumEligibilityDuration $AllowPermanentEligibility -entraRole } #active assignement limits if ( $PSBoundParameters.ContainsKey('MaximumActiveAssignmentDuration') -or ( $PSBoundParameters.ContainsKey('AllowPermanentActiveAssignment'))) { #if values are not set, use the ones from the curent config write-verbose "Maximum Active duration from curent config: $($script:config.MaximumActiveAssignmentDuration)" if (!( $PSBoundParameters.ContainsKey('MaximumActiveAssignmentDuration'))) { $MaximumActiveAssignmentDuration = $script:config.MaximumActiveAssignmentDuration } if (!( $PSBoundParameters.ContainsKey('AllowPermanentActiveAssignment'))) { $AllowPermanentActiveAssignment = $script:config.AllowPermanentActiveAssignment } if ( ($false -eq $AllowPermanentActiveAssignment) -and ( ($MaximumActiveAssignmentDuration -eq "") -or ($null -eq $MaximumActiveAssignmentDuration) )){ throw "ERROR: you requested the assignement to expire but the maximum duration is not defined, please use the MaximumActiveAssignmentDuration parameter" } $rules += Set-ActiveAssignment $MaximumActiveAssignmentDuration $AllowPermanentActiveAssignment -entraRole } ################# # Notifications # ################# # Notif Eligibility assignment Alert if ($PSBoundParameters.Keys.Contains('Notification_EligibleAssignment_Alert')) { $rules += Set-Notification_EligibleAssignment_Alert $Notification_EligibleAssignment_Alert -entraRole } # Notif elligibility assignee if ($PSBoundParameters.Keys.Contains('Notification_EligibleAssignment_Assignee')) { $rules += Set-Notification_EligibleAssignment_Assignee $Notification_EligibleAssignment_Assignee -entraRole } # Notif elligibility approver if ($PSBoundParameters.Keys.Contains('Notification_EligibleAssignment_Approver')) { $rules += Set-Notification_EligibleAssignment_Approver $Notification_EligibleAssignment_Approver -entraRole } # Notif Active Assignment Alert if ($PSBoundParameters.Keys.Contains('Notification_ActiveAssignment_Alert')) { $rules += Set-Notification_ActiveAssignment_Alert $Notification_ActiveAssignment_Alert -entraRole } # Notif Active Assignment Assignee if ($PSBoundParameters.Keys.Contains('Notification_ActiveAssignment_Assignee')) { $rules += Set-Notification_ActiveAssignment_Assignee $Notification_ActiveAssignment_Assignee -entraRole } # Notif Active Assignment Approvers if ($PSBoundParameters.Keys.Contains('Notification_ActiveAssignment_Approver')) { $rules += Set-Notification_ActiveAssignment_Approver $Notification_ActiveAssignment_Approver -entraRole } # Notification Activation alert if ($PSBoundParameters.Keys.Contains('Notification_Activation_Alert')) { $rules += Set-Notification_Activation_Alert $Notification_Activation_Alert -entraRole } # Notification Activation Assignee if ($PSBoundParameters.Keys.Contains('Notification_Activation_Assignee')) { $rules += Set-Notification_Activation_Assignee $Notification_Activation_Assignee -entraRole } # Notification Activation Approvers if ($PSBoundParameters.Keys.Contains('Notification_Activation_Approver')) { $rules += Set-Notification_Activation_Approver $Notification_Activation_Approver -entraRole } # Bringing all the rules together and patch the policy $allrules = $rules -join ',' #Write-Verbose "All rules: $allrules" #Patching the policy if ($PSCmdlet.ShouldProcess($_, "Udpdating policy")) { $null = Update-EntraRolePolicy $script:config.policyID $allrules } } log "Success, policy updated" return } catch { MyCatch $_ } } <# .Synopsis Set the setting for the owner and member roles of a group .Description set the setting for the owner and member roles of a group .Example PS> Set-PIMGroupPolicy -tenantID $tenantID -groupID $gID -ActivationDuration "PT8H" -type "owner" Limit the maximum activation duration to 8h for owner role of the group $gID .EXAMPLE PS> Set-PIMGroupPolicy -tenantID $tenantID -groupID $gID -type member -ActivationDuration "P1D" -ApprovalRequired $true -Approvers @(@{"Id"="25f3deb5-1c8d-4035-942d-b3cbbad98b8e";"Name"="John";"Type"="user"}) -Notification_EligibleAssignment_Alert @{"isDefaultRecipientEnabled"="true"; "notificationLevel"="All";"Recipients" = @("email1@domain.com","email2@domain.com")} Require approval on activation and set John as an approver, configure some notifications for the member role of the group $gIDs .Link .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Set-PIMGroupPolicy { [CmdletBinding(DefaultParameterSetName = 'Default', SupportsShouldProcess = $true)] [OutputType([bool])] param ( [Parameter(Position = 0, Mandatory = $true)] [System.String] # Tenant ID $tenantID, [Parameter(Position = 1, Mandatory = $true)] [System.String[]] #list of group to update $groupID, [Parameter(Position = 2, Mandatory = $true)] [System.String] # type of role (owner or member) $type, [System.String] # Maximum activation duration $ActivationDuration, [Parameter(HelpMessage = "Accepted values: 'None' or any combination of these options (Case SENSITIVE): 'Justification, 'MultiFactorAuthentication', 'Ticketing'")] [ValidateScript({ # accepted values: "None","Justification", "MultiFactorAuthentication", "Ticketing" # WARNING: options are CASE SENSITIVE $script:valid = $true $acceptedValues = @("None", "Justification", "MultiFactorAuthentication", "Ticketing") $_ | ForEach-Object { if (!( $acceptedValues -Ccontains $_)) { $script:valid = $false } } return $script:valid })] [System.String[]] # Activation requirement $ActivationRequirement, [Parameter(HelpMessage = "Accepted values: 'None' or any combination of these options (Case SENSITIVE): 'Justification, 'MultiFactorAuthentication'")] [ValidateScript({ # accepted values: "None","Justification", "MultiFactorAuthentication" # WARNING: options are CASE SENSITIVE $script:valid = $true $acceptedValues = @("None", "Justification", "MultiFactorAuthentication") $_ | ForEach-Object { if (!( $acceptedValues -Ccontains $_)) { $script:valid = $false } } return $script:valid })] [System.String[]] # Active assignment requirement $ActiveAssignmentRequirement, [Parameter()] [Bool] # Is authentication context required? ($true|$false) $AuthenticationContext_Enabled, [Parameter()] [String] # Authentication context value? (ex c1) $AuthenticationContext_Value, [Parameter()] [Bool] # Is approval required to activate a role? ($true|$false) $ApprovalRequired, [Parameter()] # Array of approvers in the format: @(@{"Id"=<ObjectID>;"Name"="John":"Type"="user|group"}, .... ) $Approvers, [Parameter()] [System.String] # Maximum Eligility Duration $MaximumEligibilityDuration = $null, [Parameter()] [Bool] # Allow permanent eligibility? ($true|$false) $AllowPermanentEligibility, [Parameter()] [System.String] # Maximum active assignment duration # Duration ref https://en.wikipedia.org/wiki/ISO_8601#Durations $MaximumActiveAssignmentDuration = $null, [Parameter()] [Bool] # Allow permanent active assignement? ($true|$false) $AllowPermanentActiveAssignment, [Parameter()] [System.Collections.Hashtable] # Admin Notification when eligible role is assigned # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_EligibleAssignment_Alert, [Parameter()] [System.Collections.Hashtable] # End user notification when eligible role is assigned # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_EligibleAssignment_Assignee, [Parameter()] [System.Collections.Hashtable] # Approver notification when eligible role is assigned # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_EligibleAssignment_Approver, [Parameter()] [System.Collections.Hashtable] # Admin Notification when an active role is assigned # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_ActiveAssignment_Alert, [Parameter()] [System.Collections.Hashtable] # End user Notification when an active role is assigned # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_ActiveAssignment_Assignee, [Parameter()] [System.Collections.Hashtable] # Approver Notification when an active role is assigned # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_ActiveAssignment_Approver, [Parameter()] [System.Collections.Hashtable] # Admin Notification when a is activated # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_Activation_Alert, [Parameter()] [System.Collections.Hashtable] # End user Notification when a role is activated # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_Activation_Assignee, [Parameter()] [System.Collections.Hashtable] # Approvers Notification when a role is activated # Format: @{"isDefaultRecipientEnabled"="true|false"; "notificationLevel"="All|Critical"};"Recipients" = @("email1@domain.com","email2@domain.com")} $Notification_Activation_Approver ) try { $p = @() $PSBoundParameters.Keys | ForEach-Object { $p += "$_ =>" + $PSBoundParameters[$_] } $p = $p -join ', ' log "Function Set-PIMGroupPolicy is starting with parameters: $p" -noEcho $script:tenantID = $tenantID #at least one approver required if approval is enable # todo chech if a parameterset would be better if ($ApprovalRequired -eq $true -and $null -eq $Approvers ) { throw "`n /!\ At least one approver is required if approval is enable, please set -Approvers parameter`n`n" } $groupID | ForEach-Object { $script:config = get-Groupconfig $_ -type $type $rules = @() if ($PSBoundParameters.Keys.Contains('ActivationDuration')) { $rules += Set-ActivationDuration $ActivationDuration -EntraRole } if ($PSBoundParameters.Keys.Contains('ActivationRequirement')) { $rules += Set-ActivationRequirement $ActivationRequirement -EntraRole } if ($PSBoundParameters.Keys.Contains('ActiveAssignmentRequirement')) { $rules += Set-ActiveAssignmentRequirement $ActiveAssignmentRequirement -EntraRole } if ($PSBoundParameters.Keys.Contains('AuthenticationContext_Enabled')) { if (!($PSBoundParameters.Keys.Contains('AuthenticationContext_Value'))) { $AuthenticationContext_Value = $null } $rules += Set-AuthenticationContext $AuthenticationContext_Enabled $AuthenticationContext_Value -entraRole } # Approval and approvers if ( ($PSBoundParameters.Keys.Contains('ApprovalRequired')) -or ($PSBoundParameters.Keys.Contains('Approvers'))) { $rules += Set-Approval $ApprovalRequired $Approvers -EntraRole } # eligibility assignement if ( $PSBoundParameters.ContainsKey('MaximumEligibilityDuration') -or ( $PSBoundParameters.ContainsKey('AllowPermanentEligibility'))) { #if values are not set, use the ones from the curent config write-verbose "Maximum Eligibiliy duration from curent config: $($script:config.MaximumEligibleAssignmentDuration)" if (!( $PSBoundParameters.ContainsKey('MaximumEligibilityDuration'))) { $MaximumEligibilityDuration = $script:config.MaximumEligibleAssignmentDuration } if (!( $PSBoundParameters.ContainsKey('AllowPermanentEligibility'))) { $AllowPermanentEligibility = $script:config.AllowPermanentEligibleAssignment } if ( ($false -eq $AllowPermanentEligibility) -and ( ($MaximumEligibilityDuration -eq "") -or ($null -eq $MaximumEligibilityDuration) )) { throw "ERROR: you requested the assignement to expire but the maximum duration is not defined, please use the MaximumEligibilityDuration parameter" } $rules += Set-EligibilityAssignment $MaximumEligibilityDuration $AllowPermanentEligibility -entraRole } #active assignement limits if ( $PSBoundParameters.ContainsKey('MaximumActiveAssignmentDuration') -or ( $PSBoundParameters.ContainsKey('AllowPermanentActiveAssignment'))) { #if values are not set, use the ones from the curent config write-verbose "Maximum Active duration from curent config: $($script:config.MaximumActiveAssignmentDuration)" if (!( $PSBoundParameters.ContainsKey('MaximumActiveAssignmentDuration'))) { $MaximumActiveAssignmentDuration = $script:config.MaximumActiveAssignmentDuration } if (!( $PSBoundParameters.ContainsKey('AllowPermanentActiveAssignment'))) { $AllowPermanentActiveAssignment = $script:config.AllowPermanentActiveAssignment } if ( ($false -eq $AllowPermanentActiveAssignment) -and ( ($MaximumActiveAssignmentDuration -eq "") -or ($null -eq $MaximumActiveAssignmentDuration) )) { throw "ERROR: you requested the assignement to expire but the maximum duration is not defined, please use the MaximumActiveAssignmentDuration parameter" } $rules += Set-ActiveAssignment $MaximumActiveAssignmentDuration $AllowPermanentActiveAssignment -entraRole } ################# # Notifications # ################# # Notif Eligibility assignment Alert if ($PSBoundParameters.Keys.Contains('Notification_EligibleAssignment_Alert')) { $rules += Set-Notification_EligibleAssignment_Alert $Notification_EligibleAssignment_Alert -entraRole } # Notif elligibility assignee if ($PSBoundParameters.Keys.Contains('Notification_EligibleAssignment_Assignee')) { $rules += Set-Notification_EligibleAssignment_Assignee $Notification_EligibleAssignment_Assignee -entraRole } # Notif elligibility approver if ($PSBoundParameters.Keys.Contains('Notification_EligibleAssignment_Approver')) { $rules += Set-Notification_EligibleAssignment_Approver $Notification_EligibleAssignment_Approver -entraRole } # Notif Active Assignment Alert if ($PSBoundParameters.Keys.Contains('Notification_ActiveAssignment_Alert')) { $rules += Set-Notification_ActiveAssignment_Alert $Notification_ActiveAssignment_Alert -entraRole } # Notif Active Assignment Assignee if ($PSBoundParameters.Keys.Contains('Notification_ActiveAssignment_Assignee')) { $rules += Set-Notification_ActiveAssignment_Assignee $Notification_ActiveAssignment_Assignee -entraRole } # Notif Active Assignment Approvers if ($PSBoundParameters.Keys.Contains('Notification_ActiveAssignment_Approver')) { $rules += Set-Notification_ActiveAssignment_Approver $Notification_ActiveAssignment_Approver -entraRole } # Notification Activation alert if ($PSBoundParameters.Keys.Contains('Notification_Activation_Alert')) { $rules += Set-Notification_Activation_Alert $Notification_Activation_Alert -entraRole } # Notification Activation Assignee if ($PSBoundParameters.Keys.Contains('Notification_Activation_Assignee')) { $rules += Set-Notification_Activation_Assignee $Notification_Activation_Assignee -entraRole } # Notification Activation Approvers if ($PSBoundParameters.Keys.Contains('Notification_Activation_Approver')) { $rules += Set-Notification_Activation_Approver $Notification_Activation_Approver -entraRole } # Bringing all the rules together and patch the policy $allrules = $rules -join ',' #Write-Verbose "All rules: $allrules" #Patching the policy if ($PSCmdlet.ShouldProcess($_, "Udpdating policy")) { $null = Update-EntraRolePolicy $script:config.policyID $allrules } } log "Success, policy updated" return } catch { MyCatch $_ } } <# .Synopsis Visualize PIM activities .Description Visualire PIM activities .Example PS> Get-PIMReport -tennantID $tenantID .Notes Author: Loïc MICHEL Homepage: https://github.com/kayasax/EasyPIM #> function Show-PIMReport { [CmdletBinding(DefaultParameterSetName = 'Default')] [OutputType([System.Object[]])] param ( [Parameter(Position = 0, Mandatory = $true)] [System.String] # Tenant ID $tenantID, [Parameter(Position = 1, Mandatory = $false)] [System.String] # upn of the user $upn ) try { $Script:tenantID = $tenantID $allresults = @() #$top = 100 $endpoint = "auditlogs/directoryAudits?`$filter=loggedByService eq 'PIM'" #&`$top=$top" $result = invoke-graph -Endpoint $endpoint -Method "GET" $allresults += $result.value if ($result."@odata.nextLink") { do { $endpoint = $result."@odata.nextLink" -replace "https://graph.microsoft.com/v1.0/", "" $result = invoke-graph -Endpoint $endpoint -Method "GET" $allresults += $result.value } until( !($result."@odata.nextLink") ) } #filter activities from the PIM service and completed activities $allresults = $allresults | Where-Object { $null -ne $_.initiatedby.values.userprincipalname } | Where-Object { $_.activityDisplayName -notmatch "completed" } #check if upn parameter is set using psboundparameters if ($PSBoundParameters.ContainsKey('upn')) { Write-Verbose "Filtering activities for $upn" $allresults = $allresults | Where-Object {$_.initiatedby.values.userprincipalname -eq $upn} if ($allresults.count -eq 0) { Write-Warning "No activity found for $upn" return } } $Myoutput = @() $allresults | ForEach-Object { $props = @{} $props["activityDateTime"] = $_.activityDateTime $props["activityDisplayName"] = $_.activityDisplayName $props["category"] = $_.category $props["operationType"] = $_.operationType $props["result"] = $_.result $props["resultReason"] = $_.resultReason $props["initiatedBy"] = $_.initiatedBy.values.userprincipalname $props["role"] = $_.targetResources[0]["displayname"] if ( ($_.targetResources | Measure-Object).count -gt 2) { if ($_.targetResources[2]["type"] -eq "User") { $props["targetUser"] = $_.targetResources[2]["userprincipalname"] } elseif ($_.targetResources[2]["type"] -eq "Group") { $props["targetGroup"] = $_.targetResources[2]["displayname"] } $props["targetResources"] = $_.targetResources[3]["displayname"] } else { $props["targetResources"] = $_.targetResources[0].displayname } $Myoutput += New-Object PSObject -Property $props } $Myoutput #Data for the HTML report $props = @{} $stats_category = @{} $categories = $Myoutput | Group-Object -Property category $categories | ForEach-Object { $stats_category[$_.Name] = $_.Count } $props["category"] = $stats_category $stats_requestor = @{} $requestors = $Myoutput | Group-Object -Property initiatedBy | Sort-Object -Property Count -Descending | select-object -first 10 $requestors | ForEach-Object { $stats_requestor[$_.Name] = $_.Count } $props["requestor"] = $stats_requestor $stats_result = @{} $results = $Myoutput | Group-Object -Property result $results | ForEach-Object { $stats_result[$_.Name] = $_.Count } $props["result"] = $stats_result $stats_activity = @{} $activities = $Myoutput | Group-Object -Property activityDisplayName $activities | ForEach-Object { if ($_.Name -notmatch "completed") { $stats_activity[$_.Name] = $_.Count } } $props["activity"] = $stats_activity $stats_group = @{} $targetgroup = $Myoutput | Where-Object { $_.category -match "group" } | Group-Object -Property targetresources | Sort-Object -Property Count -Descending | select-object -first 10 $targetgroup | ForEach-Object { $stats_group[$_.Name] = $_.Count } $props["targetgroup"] = $stats_group $stats_resource = @{} $targetresource = $Myoutput | Where-Object { $_.category -match "resource" } | Group-Object -Property role | Sort-Object -Property Count -Descending | select-object -first 10 $targetresource | ForEach-Object { $stats_resource[$_.Name] = $_.Count } $props["targetresource"] = $stats_resource $stats_role = @{} $targetrole = $Myoutput | Where-Object { $_.category -match "role" } | Group-Object -Property role | Sort-Object -Property Count -Descending | select-object -first 10 $targetrole | ForEach-Object { $stats_role[$_.Name] = $_.Count } $props["targetrole"] = $stats_role $props["startdate"]=($Myoutput | Sort-Object -Property activityDateTime | Select-Object -First 1).activityDateTime $props["enddate"]=($Myoutput | Sort-Object -Property activityDateTime -Descending | Select-Object -First 1).activityDateTime #$props #building the dynamic part of the report $myscript = " <script> Chart.defaults.plugins.title.font.size = 18; Chart.defaults.plugins.title.color='#DDDDDD'; Chart.defaults.plugins.legend.labels.color='#ffff99'; Chart.defaults.scale.ticks.color = '#ffff99'; const ctx = document.getElementById('myChart'); new Chart(ctx, { type: 'pie', data: { labels: [" $props.category.Keys | ForEach-Object { $myscript += "'" + $_ + "'," } $myscript = $myscript.Replace(",$", "") #remove the last comma $myscript += "], datasets: [{ label: '# of activities', data: [" $props.category.Keys | ForEach-Object { $myscript += "'" + $props.category[$_] + "'," } $myscript = $myscript.Replace(",$", "") #remove the last comma $myscript += "], hoverOffset: 10 }] }, options: { responsive: false, radius: 70, layout: { padding: { left: 10, // Adjust this value to push the chart to the left } }, plugins: { legend: { display: true, position: 'right', }, title: { display: true, text: 'Category', position: 'top', padding: { top: 10 } } } } }); const ctx4 = document.getElementById('activities'); new Chart(ctx4, { type: 'pie', data: { labels: [" $props.activity.Keys | ForEach-Object { $myscript += "'" + $_ + "'," } $myscript = $myscript.Replace(",$", "") #remove the last comma $myscript += "], datasets: [{ label: '# of activities', data: [" $props.activity.Keys | ForEach-Object { $myscript += "'" + $props.activity[$_] + "'," } $myscript = $myscript.Replace(",$", "") #remove the last comma $myscript += "], hoverOffset: 10 }] }, options: { responsive: false, radius: 70, layout: { padding: { left: 10, // Adjust this value to push the chart to the left } }, plugins: { legend: { display: true, position: 'right', }, title: { display: true, text: 'Activity type', position: 'top', padding: { top: 10 } } } } }); const ctx2 = document.getElementById('result'); new Chart(ctx2, { type: 'pie', data: { labels: ['Success', 'Failure'], datasets: [{ label: 'result', data: ['" $myscript += $props.result['success'] $myscript += "','" $myscript += $props.result['failure'] $myscript += "'" $myscript += "], backgroundColor: [ 'rgb(0, 255, 0)', 'rgb(255, 0, 0)' ], hoverOffset: 10 }] }, options: { responsive: false, radius: 70, layout: { padding: { left: 10, // Adjust this value to push the chart to the left } }, plugins: { legend: { display: true, position: 'right', }, title: { display: true, text: 'Result', position: 'top', padding: { top: 10 } }, } } }); const ctx3 = document.getElementById('requestor'); new Chart(ctx3, { type: 'bar', data: { labels: [" $props.requestor.Keys | ForEach-Object { $myscript += "'" + $_ + "'," } $myscript = $myscript.Replace(",$", "") #remove the last comma $myscript += "], datasets: [{ label: 'Number of requests', data: [" $props.requestor.Keys | ForEach-Object { $myscript += "'" + $props.requestor[$_] + "'," } $myscript = $myscript.Replace(",$", "") #remove the last comma $myscript += "], hoverOffset: 10 }] }, options: { responsive: false, indexAxis: 'y', plugins: { legend: { display: false, position: 'right', }, title: { display: true, text: 'Top 10 Requestors', position: 'top', padding: { top: 10 } }, } } }); const ctx5 = document.getElementById('Groups'); new Chart(ctx5, { type: 'bar', data: { labels: [" $props.targetGroup.Keys | ForEach-Object { $myscript += "'" + $_ + "'," } $myscript = $myscript.Replace(",$", "") #remove the last comma $myscript += "], datasets: [{ label: 'Number of requests', data: [" $props.targetGroup.Keys | ForEach-Object { $myscript += "'" + $props.targetGroup[$_] + "'," } $myscript = $myscript.Replace(",$", "") #remove the last comma $myscript += "], hoverOffset: 10 }] }, options: { responsive: false, indexAxis: 'y', plugins: { legend: { display: false, position: 'right', }, title: { display: true, text: 'Top 10 Groups requested', position: 'top', padding: { top: 10 } }, } } }); const ctx6 = document.getElementById('Resources'); new Chart(ctx6, { type: 'bar', data: { labels: [" $props.targetResource.Keys | ForEach-Object { $myscript += "'" + $_ + "'," } $myscript = $myscript.Replace(",$", "") #remove the last comma $myscript += "], datasets: [{ label: 'Number of requests', data: [" $props.targetresource.Keys | ForEach-Object { $myscript += "'" + $props.targetresource[$_] + "'," } $myscript = $myscript.Replace(",$", "") #remove the last comma $myscript += "], hoverOffset: 10 }] }, options: { responsive: false, indexAxis: 'y', plugins: { legend: { display: false, position: 'right', }, title: { display: true, text: 'Top 10 Azure role requested', position: 'top', padding: { top: 10 } }, } } }); const ctx7 = document.getElementById('Roles'); new Chart(ctx7, { type: 'bar', data: { labels: [" $props.targetrole.Keys | ForEach-Object { $myscript += "'" + $_ + "'," } $myscript = $myscript.Replace(",$", "") #remove the last comma $myscript += "], datasets: [{ label: 'Number of requests', data: [" $props.targetrole.Keys | ForEach-Object { $myscript += "'" + $props.targetrole[$_] + "'," } $myscript = $myscript.Replace(",$", "") #remove the last comma $myscript += "], hoverOffset: 10 }] }, options: { responsive: false, indexAxis: 'y', plugins: { legend: { display: false, position: 'right', }, title: { display: true, text: 'Top 10 Entra role requested', position: 'top', padding: { top: 10 } }, } } }); </script> </body> </html>" #$myscript $html = @' <html> <head> <title>EasyPIM: Activity summary</title> </head> <style> body { background-color: #2b2b2b; color: #f5f5f5; } #container { background-color: #3c3c3c; display: flex; flex-direction: column; justify-content: space-between; /* Optional: Adds some space between the divs */ } .row { display: flex; padding: 10px; border-bottom: 1px solid #444; } .chart { flex: 1; /* Optional: Each div will take up an equal amount of space */ } .description { flex: 1; /* Optional: Each div will take up an equal amount of space */ vertical-align: middle; color:#a7a7a7; } code { font-family: Consolas, "Courier New", monospace; background-color: #203048; color: #f5f5f5; padding: 0.2em 0.4em; font-size: 85%; border-radius: 6px; line-height: 1.5; } #fixedDiv { background-color: #3c3c3c; color: #f5f5f5; position: fixed; top: 10; left: 980; width: 200px; /* Adjust as needed */ height: 200px; /* Adjust as needed */ padding: 10px; /* Adjust as needed */ z-index: 1000; /* Ensure the div stays on top of other elements */ } a { color: #1cd031; } H1,H2{ text-align: center; } .header{ border-bottom: #444 1px solid; } .footer{ text-align: center; color: #a7a7a7; } </style> <body> <div id="fixedDiv">Navigation <ul> <li><a href="#myChart">Category</a></li> <li><a href="#result">Result</a></li> <li><a href="#activities">Activities</a></li> <li><a href="#requestor">Requestor</a></li> <li><a href="#Groups">Groups</a></li> <li><a href="#Resources">Azure Roles</a></li> <li><a href="#Roles">Entra Roles</a></li> </ul> </div> <div id="container" style="width: 950px"> <div class="header"> <h1>PIM activity summary</h1> <h2>from '@ $html+= $props['startdate'].ToString() + " to " + $props['enddate'].ToString() + "</h2></div>" $html += @' <div class="row"> <div class="chart"> <canvas id="myChart" width="900" height="200"></canvas> </div> </div> <div class="row"> <div class="description"> Assuming this page was generated with <code>$r=show-PIMreport</code>, you can use the following code to filter the activity for a specific category:<br> <pre><code>$r | where-object { $_.category -eq "GroupManagement" }</code></pre> </div> </div> <div class="row"> <div class="chart"> <canvas id="result" width="900" height="200"></canvas> </div> </div> <div class="row"> <div class="description"> Assuming this page was generated with <code>$r=show-PIMreport</code>, you can use the following code to consult the failed operations:<br> <code>$r | where-object {$_.result -eq "Failure"}</code> </div> </div> <div class="row"> <div class="chart"> <canvas id="activities" width="900" height="400"></canvas> </div> </div> <div class="row"> <div class="description">Assuming this page was generated with <code>$r=show-PIMreport</code>, you can use the following code to consult the details:<br> <code>$r | where-object {$_.activityDisplayName -eq "Add member to role in PIM requested (timebound)"}</code> </div> </div> <div class="row"> <div class="chart"> <canvas id="requestor" width="900" height="500"></canvas> </div> </div> <div class="row"> <div class="description">Assuming this page was generated with <code>$r=show-PIMreport</code>, you can use the following code to filter the activity requested by User1:<br> <code>$r | where-object {$_.Initiatedby -match "user1"}</code> </div> </div> <div class="row"> <div class="chart"> <canvas id="Groups" width="900" height="500"></canvas> </div> </div> <div class="row"> <div class="description">Assuming this page was generated with <code>$r=show-PIMreport</code>, you can use the following code to get the details for a group:<br> <code>$r | where-object {$_.category -match "group" -and $_.targetresources -eq "PIM_GuestAdmins"}</code> </div> </div> <div class="row"> <div class="chart"> <canvas id="Resources" width="900" height="500"></canvas> </div> </div> <div class="row"> <div class="description">Assuming this page was generated with <code>$r=show-PIMreport</code>, you can use the following code to consult the details for a specific Azure role:<br> <code>$r | where-object {$_.category -match "resource" -and $_.role -eq "Reader"}</code> </div> </div> <div class="row"> <div class="chart"> <canvas id="Roles" width="900" height="500"></canvas> </div> </div> <div class="row"> <div class="description">Assuming this page was generated with <code>$r=show-PIMreport</code>, you can use the following code to consult the details for a specific Enntra role:<br> <code>$r | where-object {$_.category -match "role" -and $_.role -eq "Global Administrator"}</code> </div> </div> <div class='footer'> <p>Generated with <a href='https://powershellgallery.com/packages/EasyPIM'>EasyPIM</a></p> </div> </div> <!-- container --> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> '@ $html += $myscript $html | Out-File -FilePath "$env:temp\PIMReport.html" -Force invoke-item "$env:temp\PIMReport.html" } catch { MyCatch $_ } } #*************************************** #* CONFIGURATION #*************************************** write-verbose "variables.ps1 called" # LOG TO FILE ( if enable by default it will create a LOGS subfolder in the script folder, and create a logfile with the name of the script ) $script:logToFile = $false # Where logs are written to $script:_logPath = "$env:appdata\powershell\easyPIM" # TEAMS NOTIDICATION # set to $true if you want to send fatal error on a Teams channel using Webhook see doc to setup $script:TeamsNotif = $false #The description will be used as the notification subject $script:description = "EasyPIM module to manage Azure role setting" # your Teams Inbound WebHook URL $script:teamsWebhookURL = "https://microsoft.webhook.office.com/webhookb2/xxxxxxx/IncomingWebhook/xxxxxxxxxxxxxx" #*************************************** #* PRIVATE VARIABLES DON'T TOUCH !! #*************************************** #from now every error will be treated as exception and terminate the script $script:_scriptFullName = $MyInvocation.scriptName $script:_scriptName = "EasyPIM" #Split-Path -Leaf $_scriptFullName $script:HostFQDN = $env:computername + "." + $env:USERDNSDOMAIN # ERROR HANDLING $ErrorActionPreference = "STOP" # make all errors terminating ones so they can be catched #checking new version of easyPIM try { $currentVersion = (get-module easypim -listavailable| Sort-Object version -desc |Select-Object -first 1).version.toString() Write-Verbose $currentVersion $latestVersion = (Find-Module -Name EasyPIM).Version write-verbose $latestVersion if ($currentVersion -lt $latestVersion) { Write-Host "🔥 FYI: A newer version of EasyPIM is available! Run the command below to update to the latest version." Write-Host "💥 Installed version: $currentVersion → Latest version: $latestVersion" -ForegroundColor DarkGray Write-Host "✨ Update-Module EasyPIM" -NoNewline -ForegroundColor Green Write-Host " → Install the latest version of EasyPIM." -ForegroundColor Yellow #return $true } } catch { Write-Verbose -Message $_} |