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
}

# Define shared helper functions for cleanup operations
# Used by Invoke-Cleanup
#[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "", Justification="Module includes an alias with plural form for backward compatibility")]
#param()

# Define protected roles that should never be removed automatically at script level
$script:protectedRoles = @(
    "User Access Administrator",
    "Global Administrator",
    "Privileged Role Administrator",
    "Security Administrator"
)

# Define script-level counters at the top of the file (outside any function)
$script:keptCounter = 0
$script:removeCounter = 0
$script:skipCounter = 0
$script:protectedCounter = 0

function Test-IsProtectedAssignment {
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory = $true)]
        [string]$PrincipalId,

        [Parameter(Mandatory = $true)]
        [array]$ProtectedUsers
    )

    return $ProtectedUsers -contains $PrincipalId
}

function Test-IsProtectedRole {
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory = $true)]
        [string]$RoleName
    )

    return $script:protectedRoles -contains $RoleName
}

function Test-AssignmentInConfig {
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory = $true)]
        [string]$PrincipalId,

        [Parameter(Mandatory = $true)]
        [string]$RoleName,

        [Parameter(Mandatory = $false)]
        [string]$Scope,

        [Parameter(Mandatory = $false)]
        [string]$GroupId,

        [Parameter(Mandatory = $true)]
        [array]$ConfigAssignments,

        [Parameter(Mandatory = $false)]
        [string]$ResourceType = "Azure"
    )

    # Base matching logic for all resource types
    foreach ($config in $ConfigAssignments) {
        # Check role name match - handle various property possibilities
        $roleMatches = $false
        foreach ($propName in @("RoleName", "Rolename", "Role", "roleName", "rolename", "role")) {
            if ($config.PSObject.Properties.Name -contains $propName) {
                $roleMatches = $config.$propName -ieq $RoleName
                if ($roleMatches) { break }
            }
        }

        # Check principal ID match - direct match
        $principalMatches = $false
        foreach ($propName in @("PrincipalId", "principalId", "PrincipalID", "principalID")) {
            if ($config.PSObject.Properties.Name -contains $propName) {
                $principalMatches = $config.$propName -eq $PrincipalId
                if ($principalMatches) { break }
            }
        }

        # If not matched directly, check in PrincipalIds array if present
        if (-not $principalMatches -and $config.PSObject.Properties.Name -contains "PrincipalIds") {
            $principalMatches = $config.PrincipalIds -contains $PrincipalId
        }

        # Different matching logic based on resource type
        $typeMatches = $false

        switch ($ResourceType) {
            "Azure" {
                if (-not $Scope) {
                    $typeMatches = $true # No scope to check
                } else {
                    # Check scope match
                    $scopeMatches = $false
                    foreach ($propName in @("Scope", "scope")) {
                        if ($config.PSObject.Properties.Name -contains $propName) {
                            $scopeMatches = $config.$propName -eq $Scope
                            if ($scopeMatches) { break }
                        }
                    }
                    $typeMatches = $scopeMatches
                }
            }
            "Entra" {
                # For Entra roles, check for directoryScopeId if available
                if (-not $Scope) {
                    # If no scope provided, it's tenant-wide
                    $typeMatches = $true
                } else {
                    # If scope is provided and matches an Administrative Unit format, check for DirectoryScopeId property
                    # or assume it's tenant-wide if not specified in config
                    $scopeMatches = $false
                    foreach ($propName in @("DirectoryScopeId", "directoryScopeId")) {
                        if ($config.PSObject.Properties.Name -contains $propName) {
                            $scopeMatches = $config.$propName -eq $Scope
                            if ($scopeMatches) { break }
                        }
                    }
                    # If the config has no DirectoryScopeId, assume it's tenant-wide and doesn't match AU-scoped role
                    $typeMatches = $scopeMatches
                }
            }
            "Group" {
                if (-not $GroupId) {
                    $typeMatches = $false # Group ID is required
                } else {
                    # Check group ID match
                    $groupMatches = $false
                    foreach ($propName in @("GroupId", "groupId", "GroupID", "groupID")) {
                        if ($config.PSObject.Properties.Name -contains $propName) {
                            $groupMatches = $config.$propName -eq $GroupId
                            if ($groupMatches) { break }
                        }
                    }
                    $typeMatches = $groupMatches
                }
            }
            default {
                $typeMatches = $true # Default to true if resource type is not specified
            }
        }

        # Return true if all required components match
        if ($principalMatches -and $roleMatches -and $typeMatches) {
            return $true
        }
    }

    return $false
}

function Get-FormattedCleanupSummary {
    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        [Parameter(Mandatory = $true)]
        [string]$ResourceType,

        [Parameter(Mandatory = $true)]
        [int]$KeptCount,

        [Parameter(Mandatory = $true)]
        [int]$RemovedCount,

        [Parameter(Mandatory = $true)]
        [int]$SkippedCount,

        [Parameter(Mandatory = $false)]
        [int]$ProtectedCount = 0
    )

    $output = @"
┌────────────────────────────────────────────────────┐
│ $ResourceType Cleanup Summary |
├────────────────────────────────────────────────────┤
│ ✅ Kept: $KeptCount
│ 🗑️ Removed: $RemovedCount
│ ⏭️ Skipped: $SkippedCount
"@


    if ($ProtectedCount -gt 0) {
        $output += "`n│ 🛡️ Protected: $ProtectedCount"
    }

    $output += "`n└────────────────────────────────────────────────────┘"

    return $output
}

function Reset-CleanupCounter {
    [CmdletBinding()]
    [OutputType([System.Void])]
    param()

    $script:keptCounter = 0
    $script:removeCounter = 0
    $script:skipCounter = 0
    $script:protectedCounter = 0
}

# Create an alias for backward compatibility
Set-Alias -Name Reset-CleanupCounters -Value Reset-CleanupCounter -Scope Global

function Get-AssignmentProperties {
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    [OutputType([System.Collections.Hashtable])]
    param (
        [Parameter(Mandatory = $true)]
        [object]$Assignment
    )

    # Extract principal ID
    $principalId = $null
    foreach ($propName in @("PrincipalId", "principalId", "SubjectId", "subjectId")) {
        if ($Assignment.PSObject.Properties.Name -contains $propName -and $Assignment.$propName) {
            $principalId = $Assignment.$propName
            Write-Verbose "Found principalId in property '$propName': $principalId"
            break
        }
    }

    # Extract role name/type - special handling for Group assignments
    $roleName = "Unknown"

    # First try to get accessId which is the authoritative source for Group PIM assignments
    if ($Assignment.PSObject.Properties.Name -contains 'accessId') {
        $roleName = $Assignment.accessId
        Write-Verbose "Found role in accessId: $roleName"
    }
    # Then try other Group-specific properties
    elseif ($Assignment.PSObject.Properties.Name -contains 'memberType') {
        $roleName = $Assignment.memberType
        Write-Verbose "Found role in memberType: $roleName"
    }
    else {
        foreach ($propName in @("memberType", "type", "Type", "MemberType")) {
            if ($Assignment.PSObject.Properties.Name -contains $propName -and $Assignment.$propName) {
                $roleName = $Assignment.$propName
                Write-Verbose "Found role in property '$propName': $roleName"
                break
            }
        }

        # If still no role found, try standard role properties
        if ($roleName -eq "Unknown") {
            foreach ($propName in @("RoleDefinitionDisplayName", "RoleName", "roleName", "displayName")) {
                if ($Assignment.PSObject.Properties.Name -contains $propName -and $Assignment.$propName) {
                    $roleName = $Assignment.$propName
                    Write-Verbose "Found role in property '$propName': $roleName"
                    break
                }
            }
        }
    }

    # Extract principal name
    $principalName = "Principal-$principalId"
    foreach ($propName in @("PrincipalDisplayName", "SubjectName", "displayName")) {
        if ($Assignment.PSObject.Properties.Name -contains $propName -and $Assignment.$propName) {
            $principalName = $Assignment.$propName
            Write-Verbose "Found principal name in property '$propName': $principalName"
            break
        }
    }

    # Try to get principal name from expanded principal object
    if ($Assignment.PSObject.Properties.Name -contains "principal" -and `
        $Assignment.principal.PSObject.Properties.Name -contains "displayName") {
        $principalName = $Assignment.principal.displayName
        Write-Verbose "Found principal name in expanded principal object: $principalName"
    }

    # Extract scope
    $scope = $null
    foreach ($propName in @("ResourceId", "scope", "Scope", "directoryScopeId", "ScopeId")) {
        if ($Assignment.PSObject.Properties.Name -contains $propName -and $Assignment.$propName) {
            $scope = $Assignment.$propName
            Write-Verbose "Found scope in property '$propName': $scope"
            break
        }
    }

    # Return properties as hashtable
    $result = @{
        PrincipalId = $principalId
        RoleName = $roleName.ToLower() # Normalize to lowercase for consistent comparison
        PrincipalName = $principalName
        Scope = $scope
    }

    Write-Verbose "Extracted properties: $($result | ConvertTo-Json -Compress)"
    return $result
}

function Test-IsJustificationFromOrchestrator {
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory = $true)]
        [object]$Assignment,

        [Parameter(Mandatory = $false)]
        [string]$JustificationFilter = "Invoke-EasyPIMOrchestrator"
    )

    # Check various properties where justification might be stored
    foreach ($propName in @("Justification", "justification", "Reason", "reason")) {
        if ($Assignment.PSObject.Properties.Name -contains $propName -and `
            $Assignment.$propName -and `
            $Assignment.$propName -like "*$JustificationFilter*") {

            # Log successful detection of orchestrator-created assignment
            Write-Verbose "Found orchestrator justification in property '$propName': $($Assignment.$propName)"
            return $true
        }
    }

    # Try to check additional properties that might contain justification in different API responses
    foreach ($propName in @("creationConditions", "scheduleInfo", "metadata")) {
        if ($Assignment.PSObject.Properties.Name -contains $propName -and $Assignment.$propName) {
            # Handle object properties that might contain justification
            if ($Assignment.$propName -is [System.Collections.IDictionary] -or `
                $Assignment.$propName.PSObject.Properties.Name -contains "justification") {

                $justification = if ($Assignment.$propName -is [System.Collections.IDictionary]) {
                    $Assignment.$propName["justification"]
                } else {
                    $Assignment.$propName.justification
                }

                if ($justification -and $justification -like "*$JustificationFilter*") {
                    Write-Verbose "Found orchestrator justification in nested property '$propName': $justification"
                    return $true
                }
            }
        }
    }

    return $false
}

function Test-AssignmentCreatedByOrchestrator {
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param (
        [Parameter(Mandatory = $true)]
        [object]$Assignment,

        [Parameter(Mandatory = $true)]
        [string]$TenantId,

        [Parameter(Mandatory = $true)]
        [string]$ResourceType,

        [Parameter(Mandatory = $false)]
        [string]$SubscriptionId = "",

        [Parameter(Mandatory = $false)]
        [string]$JustificationFilter = "Invoke-EasyPIMOrchestrator"
    )

    try {
        # Set the tenant ID for the helper functions to use
        $script:tenantID = $TenantId

        # Only set subscriptionID for Azure roles that require it
        if ($ResourceType -like "Azure Role*" -and -not [string]::IsNullOrEmpty($SubscriptionId)) {
            $script:subscriptionID = $SubscriptionId
        }

        # Extract necessary properties from the assignment
        $principalId = if ($Assignment.PSObject.Properties.Name -contains 'principalId') {
            $Assignment.principalId
        } elseif ($Assignment.PSObject.Properties.Name -contains 'PrincipalId') {
            $Assignment.PrincipalId
        } else { $null }

        if (-not $principalId) {
            Write-Verbose "No principalId found in assignment, cannot query schedule requests"
            return $false
        }

        # Check if the assignment itself contains justification information first
        if (Test-IsJustificationFromOrchestrator -Assignment $Assignment -JustificationFilter $JustificationFilter) {
            Write-Verbose "Found orchestrator justification directly in the assignment"
            return $true
        }

        # Handle Azure RBAC vs Entra ID vs Group roles differently
        if ($ResourceType -like "Azure Role*") {
            # For Azure roles, use Invoke-ARM with the proper API endpoint
            if ([string]::IsNullOrEmpty($SubscriptionId)) {
                Write-Verbose "No SubscriptionId provided for Azure role, cannot query ARM API"
                return $false
            }

            $scope = if ($Assignment.PSObject.Properties.Name -contains 'ScopeId') {
                $Assignment.ScopeId
            } elseif ($Assignment.PSObject.Properties.Name -contains 'scope') {
                $Assignment.scope
            } elseif ($Assignment.PSObject.Properties.Name -contains 'Scope') {
                $Assignment.Scope
            } else { $null }

            if (-not $scope) {
                Write-Verbose "No scope found in assignment, cannot query ARM API"
                return $false
            }

            # Extract role definition ID if available
            $roleDefinitionId = $null
            foreach ($propName in @("RoleDefinitionId", "roleDefinitionId")) {
                if ($Assignment.PSObject.Properties.Name -contains $propName -and $Assignment.$propName) {
                    $roleDefinitionId = $Assignment.$propName
                    break
                }
            }

            # Determine if we're checking eligible or active assignments
            $isEligible = $ResourceType -like "*eligible*"

            # For eligible assignments: Use roleEligibilityScheduleRequests
            # For active assignments: Use roleAssignmentScheduleRequests
            $requestType = if ($isEligible) {
                "roleEligibilityScheduleRequests"
            } else {
                "roleAssignmentScheduleRequests"
            }

            # Ensure the scope is properly formatted for the API URL
            # The API expects a scope like /subscriptions/{subscriptionId}
            if (-not $scope.StartsWith('/')) {
                $scope = "/$scope"
            }

            # Build the API URL as per documentation:
            # https://learn.microsoft.com/en-us/rest/api/authorization/role-assignment-schedule-requests/list-for-scope
            $apiUrl = "$scope/providers/Microsoft.Authorization/$requestType"
            $apiVersion = "2020-10-01"

            # Build the filter query parameter
            $filter = "principalId eq '$principalId'"

            # If we have a role definition ID, add it to the filter
            if ($roleDefinitionId) {
                $filter += " and roleDefinitionId eq '$roleDefinitionId'"
            }

            $apiUrl += "?api-version=$apiVersion&`$filter=$filter"

            Write-Verbose "Querying ARM API for schedule requests: $apiUrl"

            # Make the API call using the module's Invoke-ARM function
            $response = Invoke-ARM -restURI $apiUrl -method "GET"

            # Check if we received a valid response with results
            if ($response -and `
                $response.PSObject.Properties.Name -contains 'value' -and `
                $response.value -and `
                $response.value.Count -gt 0) {

                Write-Verbose "Found $($response.value.Count) schedule requests for principal $principalId"

                # Examine each request in the response
                foreach ($request in $response.value) {
                    # Look for the justification in the properties
                    if ($request.PSObject.Properties.Name -contains 'properties' -and `
                        $request.properties.PSObject.Properties.Name -contains 'justification') {

                        $justification = $request.properties.justification
                        Write-Verbose "Found justification in schedule request: $justification"

                        # Check if the justification matches our specific filter only
                        if ($justification -like "*$JustificationFilter*") {
                            Write-Verbose "Assignment was created by orchestrator based on justification: $justification"
                            return $true
                        }
                    }
                }

                Write-Verbose "No matching justification pattern found in any schedule requests"
            }
            else {
                Write-Verbose "No matching schedule requests found for principal $principalId at scope $scope"
            }
        }
        elseif ($ResourceType -like "Entra Role*" -or $ResourceType -like "Group*") {
            # For Entra ID roles or Group roles, use Invoke-Graph

            # Determine if it's an eligible or active role
            $requestType = if ($ResourceType -like "*eligible*") {
                "roleEligibilityScheduleRequests"
            } else {
                "roleAssignmentScheduleRequests"
            }

            # Determine if it's for directory (Entra) or groups
            $directoryType = "directory"  # Default for Entra roles
            $additionalFilter = ""

            # If it's a group role, extract the group ID and add to filter
            if ($ResourceType -like "Group*") {
                $groupId = $null

                # Extract group ID from assignment
                if ($Assignment.PSObject.Properties.Name -contains 'id' -and `
                    $Assignment.id -like "*_*") {
                    $groupId = $Assignment.id.Split('_')[0]
                } elseif ($Assignment.PSObject.Properties.Name -contains 'GroupId') {
                    $groupId = $Assignment.GroupId
                } elseif ($Assignment.PSObject.Properties.Name -contains 'groupId') {
                    $groupId = $Assignment.groupId
                }

                if (-not $groupId) {
                    Write-Verbose "No group ID found in group assignment, cannot query Graph API"
                    return $false
                }

                $additionalFilter = " and resourceId eq '$groupId'"
            }

            # Build the Graph API endpoint
            $graphEndpoint = "roleManagement/$directoryType/$requestType"
            $filter = "principalId eq '$principalId'$additionalFilter"
            $graphEndpoint += "?`$filter=$filter"

            Write-Verbose "Querying Microsoft Graph API using Invoke-Graph: $graphEndpoint"

            # Use the module's Invoke-Graph function with beta version
            $response = Invoke-Graph -Endpoint $graphEndpoint -Method "GET" -version "beta"

            # Check for orchestrator justification in the response
            if ($response -and $response.PSObject.Properties.Name -contains 'value' -and $response.value) {
                foreach ($request in $response.value) {
                    # Graph API returns justification directly at root level for Entra roles
                    if ($request.PSObject.Properties.Name -contains 'justification' -and `
                        ($request.justification -like "*$JustificationFilter*")) {

                        Write-Verbose "Found orchestrator justification in Graph API response: $($request.justification)"
                        return $true
                    }
                }
            }
        }

    }
    catch {
        Write-Verbose "Error in Test-AssignmentCreatedByOrchestrator: $_"
        Write-Verbose $_.Exception.Message
        Write-Verbose $_.ScriptStackTrace
    }

    # Default to false if no evidence found that the assignment was created by orchestrator
    return $false
}

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 Test-PIMGroupValid {
    [CmdletBinding()]
    [OutputType([bool])]
    param (
        [Parameter(Mandatory = $true)]
        [string]$GroupId,
        
        [Parameter(Mandatory = $false)]
        [switch]$SkipPIMCheck
    )
    
    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"
        
        if (-not $SkipPIMCheck) {
            if (-not (Test-GroupEligibleForPIM -GroupId $GroupId)) {
                Write-Warning "Group $GroupId is not eligible for PIM management, skipping"
                return $false
            }
        }
        
        return $true
    }
    catch {
        Write-Warning "Group $GroupId does not exist, skipping"
        return $false
    }
}

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
}

# Define script-level counters at the top of the file (outside any function)
$script:keptCounter = 0
$script:removeCounter = 0
$script:skipCounter = 0
$script:protectedCounter = 0

# Define protected roles at script level
$script:protectedRoles = @(
    "User Access Administrator",
    "Global Administrator",
    "Privileged Role Administrator",
    "Security Administrator"
)

function Invoke-Cleanup {
    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([System.Collections.Hashtable])]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
    param (
        [string]$ResourceType,
        [array]$ConfigAssignments,
        [hashtable]$ApiInfo,
        [array]$ProtectedUsers,
        [Parameter(Mandatory = $false)]
        [ValidateSet('Initial', 'Delta')]
        [string]$Mode = 'Delta',
        [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
    $script:protectedCounter = 0

    #region Prevent duplicate calls
    if (-not $script:ProcessedCleanups) { $script:ProcessedCleanups = @{} }

    $uniqueKey = "$ResourceType-$Mode"
    if ($ApiInfo.GroupId) { $uniqueKey += "-$($ApiInfo.GroupId)" }

    if ($script:ProcessedCleanups.ContainsKey($uniqueKey)) {
        Write-Host "`n⚠️ Cleanup for '$ResourceType' ($Mode mode) already processed - skipping duplicate call`n" -ForegroundColor Yellow
        return @{
            ResourceType = $ResourceType;
            KeptCount = 0;
            RemovedCount = 0;
            SkippedCount = 0;
            ProtectedCount = 0
        }
    }

    $script:ProcessedCleanups[$uniqueKey] = (Get-Date)

    Write-Host "`n┌────────────────────────────────────────────────────┐" -ForegroundColor Cyan
    Write-Host "│ Processing $ResourceType $Mode Cleanup" -ForegroundColor Cyan
    Write-Host "└────────────────────────────────────────────────────┘`n" -ForegroundColor Cyan

    # Display initial warning for Initial mode
    if ($Mode -eq 'Initial') {
        Write-Host "`n┌────────────────────────────────────────────────────┐" -ForegroundColor Yellow
        Write-Host "│ ⚠️ CAUTION: POTENTIALLY DESTRUCTIVE OPERATION" -ForegroundColor Yellow
        Write-Host "└────────────────────────────────────────────────────┘`n" -ForegroundColor Yellow
        Write-Host "This will remove ALL PIM assignments not defined in your configuration." -ForegroundColor Yellow
        Write-Host "If your protected users list is incomplete, you may lose access to critical resources!" -ForegroundColor Yellow
        Write-Host "Protected users count: $($ProtectedUsers.Count)" -ForegroundColor Yellow
        Write-Host "`n---" -ForegroundColor Yellow
        Write-Host "USAGE GUIDANCE:" -ForegroundColor Yellow
        Write-Host "• To preview changes without making them: Use -WhatIf" -ForegroundColor Yellow
        Write-Host "• To skip confirmation prompts: Use -Confirm:`$false" -ForegroundColor Yellow
        Write-Host "---`n" -ForegroundColor Yellow

        # Global confirmation for Initial mode
        $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
        }
    }

    # Define resource type specific settings
    $config = switch ($ResourceType) {
        "Azure Role eligible" {
            @{
                ApiEndpoint = "/providers/Microsoft.Authorization/roleEligibilityScheduleRequests";
                ApiVersion = "2020-10-01";
                RemoveCmd = "Remove-PIMAzureResourceEligibleAssignment";
                Filter = "status eq 'Provisioned'"
            }
        }
        "Azure Role active" {
            @{
                ApiEndpoint = "/providers/Microsoft.Authorization/roleAssignmentScheduleRequests";
                ApiVersion = "2020-10-01";
                RemoveCmd = "Remove-PIMAzureResourceActiveAssignment";
                Filter = "status eq 'Provisioned'"
            }
        }
        "Entra Role eligible" {
            @{
                ApiEndpoint = "https://graph.microsoft.com/beta/roleManagement/directory/roleEligibilityScheduleInstances"
                ApiVersion = "beta"
                RemoveCmd = "Remove-PIMEntraRoleEligibleAssignment"
                GraphBased = $true
            }
        }
        "Entra Role active" {
            @{
                ApiEndpoint = "https://graph.microsoft.com/beta/roleManagement/directory/roleAssignmentScheduleInstances"
                ApiVersion = "beta"
                RemoveCmd = "Remove-PIMEntraRoleActiveAssignment"
                GraphBased = $true
            }
        }
        "Group eligible" {
            @{
                ApiEndpoint = "https://graph.microsoft.com/beta/roleManagement/directory/roleEligibilityScheduleInstances"
                ApiVersion = "beta"
                RemoveCmd = "Remove-PIMGroupEligibleAssignment"
                GraphBased = $true
                GroupBased = $true
            }
        }
        "Group active" {
            @{
                ApiEndpoint = "https://graph.microsoft.com/beta/roleManagement/directory/roleAssignmentScheduleInstances"
                ApiVersion = "beta"
                RemoveCmd = "Remove-PIMGroupActiveAssignment"
                GraphBased = $true
                GroupBased = $true
            }
        }
        default {
            throw "Unknown resource type: $ResourceType"
        }
    }

    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 ==========="


    try {
        # Get current assignments directly using scopes from config
        Write-Host "`n=== Processing Scopes ===" -ForegroundColor Cyan
        $allAssignments = @()

        if ($config.GraphBased) {
            if ($config.GroupBased) {
                # For group assignments, need to get assignments for each group
                Write-Host " 🔍 Checking group assignments" -ForegroundColor White
                try {
                    $getCmd = if ($ResourceType -eq "Group eligible") {
                        "Get-PIMGroupEligibleAssignment"
                    } else {
                        "Get-PIMGroupActiveAssignment"
                    }

                    foreach ($groupId in $ApiInfo.GroupIds) {
                        Write-Host " ├─ Processing group: $groupId" -ForegroundColor White
                        $groupAssignments = & $getCmd -TenantId $ApiInfo.TenantId -groupId $groupId
                        if ($null -ne $groupAssignments) {
                            $allAssignments += $groupAssignments
                            Write-Host " │ └─ Found $($groupAssignments.Count) assignments" -ForegroundColor Gray
                        }
                    }
                }
                catch {
                    Write-Error "Failed to get group assignments: $_"
                }
            }
            else {
                # For Entra roles, get assignments using our module's commands
                Write-Host " 🔍 Checking tenant-wide Entra Role assignments" -ForegroundColor White
                try {
                    $getCmd = if ($ResourceType -eq "Entra Role eligible") {
                        "Get-PIMEntraRoleEligibleAssignment"
                    } else {
                        "Get-PIMEntraRoleActiveAssignment"
                    }

                    $allAssignments = & $getCmd -TenantId $ApiInfo.TenantId
                    Write-Host " ├─ Found $($allAssignments.Count) assignments" -ForegroundColor Gray
                }
                catch {
                    Write-Error "Failed to get Entra role assignments: $_"
                }
            }
        }
        else {
            # For Azure roles, use existing scope-based logic
            $getCmd = if ($ResourceType -eq "Azure Role eligible") {
                "Get-PIMAzureResourceEligibleAssignment"
            } else {
                "Get-PIMAzureResourceActiveAssignment"
            }

            # Track processed scopes to avoid duplicates
            $processedScopes = @{}

            foreach ($configAssignment in $ConfigAssignments) {
                if ($configAssignment.Scope) {
                    # Skip if we've already processed this scope
                    if ($processedScopes.ContainsKey($configAssignment.Scope)) {
                        Write-Verbose "Skipping already processed scope: $($configAssignment.Scope)"
                        continue
                    }

                    Write-Host " 🔍 Checking scope: $($configAssignment.Scope)" -ForegroundColor White
                    $params = @{
                        TenantId = $ApiInfo.TenantId
                        Scope = $configAssignment.Scope
                    }

                    $scopeAssignments = & $getCmd @params
                    if ($null -ne $scopeAssignments) {
                        $allAssignments += $scopeAssignments
                    }

                    Write-Host " ├─ Found $($scopeAssignments.Count) assignments" -ForegroundColor Gray
                    $processedScopes[$configAssignment.Scope] = $true
                }
            }
        }

        Write-Host "`n=== Processing Assignments ===" -ForegroundColor Cyan
        Write-Host " 📊 Total assignments found: $($allAssignments.Count)" -ForegroundColor White

        # Create a tracking set for processed assignments to avoid duplicates
        $processedAssignments = @{}

        # Process assignments
        if ($allAssignments.Count -gt 0) {
            foreach ($assignment in $allAssignments) {
                # Extract assignment details with proper fallbacks for each property
                $principalId = if ($config.GraphBased) {
                    $assignment.principalid  # Our module's commands provide this consistently
                }
                elseif ($null -ne $assignment.PrincipalId) {
                    $assignment.PrincipalId
                }
                elseif ($null -ne $assignment.principalId) {
                    $assignment.principalId
                }
                else {
                    $null
                }

                # Handle role name/member type based on resource type
                $roleName = if ($config.GroupBased) {
                    $assignment.memberType  # For groups, use memberType (member/owner)
                }
                elseif ($config.GraphBased) {
                    $assignment.rolename  # For Entra roles, use rolename consistently
                }
                elseif ($null -ne $assignment.RoleName -and $assignment.RoleName -ne '') {
                    $assignment.RoleName
                }
                elseif ($null -ne $assignment.roleName -and $assignment.roleName -ne '') {
                    $assignment.roleName
                }
                elseif ($null -ne $assignment.RoleDefinitionDisplayName -and $assignment.RoleDefinitionDisplayName -ne '') {
                    $assignment.RoleDefinitionDisplayName
                }
                else {
                    $roleId = $assignment.RoleId
                    if ($roleId) {
                        try {
                            $roleDefinition = Get-AzRoleDefinition -Id ($roleId -split '/')[-1]
                            if ($roleDefinition) {
                                $roleDefinition.Name
                            }
                            else { $null }
                        }
                        catch {
                            Write-Verbose "Failed to get role name from role definition: $_"
                            $null
                        }
                    }
                    else { $null }
                }

                # Get scope - handle Azure roles properly
                $scope = if ($config.GraphBased) {
                    $null  # Entra roles don't use scope
                }
                else {  # For Azure roles, always use ScopeId
                    $assignment.ScopeId
                }

                # Get principal name - simplified since our module provides consistent output
                $principalName = if ($config.GraphBased) {
                    $assignment.principalname  # Our module's commands provide this consistently
                }
                elseif ($null -ne $assignment.PrincipalName) {
                    $assignment.PrincipalName
                }
                elseif ($null -ne $assignment.principalName) {
                    $assignment.principalName
                }
                else {
                    "Principal-$principalId"
                }

                Write-Host "`n Processing: $principalName" -ForegroundColor White
                Write-Host " ├─ Role: $roleName" -ForegroundColor Gray
                if ($scope) {
                    Write-Host " ├─ Scope: $scope" -ForegroundColor Gray
                }

                # Skip invalid assignments
                if (-not $principalId -or -not $roleName) {
                    Write-Host " └─ ⚠️ Invalid assignment data (missing principalId or roleName) - skipping" -ForegroundColor Yellow
                    $script:skipCounter++
                    continue
                }
                # For non-Graph based assignments (Azure roles), require scope
                if (-not $config.GraphBased -and -not $scope) {
                    Write-Host " └─ ⚠️ Invalid assignment data (missing scope for Azure role) - skipping" -ForegroundColor Yellow
                    $script:skipCounter++
                    continue
                }

                # Create a unique key to track this assignment - for Graph-based assignments, don't include scope
                $assignmentKey = if ($config.GraphBased) {
                    "$principalId|$roleName"
                } else {
                    "$principalId|$roleName|$scope"
                }

                # Skip if we've already processed this assignment
                if ($processedAssignments.ContainsKey($assignmentKey)) {
                    Write-Host " └─ ⏭️ Duplicate entry - skipping" -ForegroundColor DarkYellow
                    $script:skipCounter++
                    continue
                }

                # Mark as processed
                $processedAssignments[$assignmentKey] = $true

                # Check if assignment matches config
                $foundInConfig = $false
                foreach ($configAssignment in $ConfigAssignments) {
                    $matchesPrincipal = $configAssignment.PrincipalId -eq $principalId
                    $matchesRole = $configAssignment.RoleName -ieq $roleName

                    # For Graph-based assignments (Entra Roles), ignore scope comparison
                    $matchesScope = if ($config.GraphBased) {
                        $true
                    } else {
                        $configAssignment.Scope -eq $scope
                    }

                    if ($matchesPrincipal -and $matchesRole -and $matchesScope) {
                        $foundInConfig = $true
                        break
                    }
                }

                # Keep assignment if it's in config
                if ($foundInConfig) {
                    Write-Host " └─ ✅ Matches config - keeping" -ForegroundColor Green
                    $script:keptCounter++
                    continue
                }

                # Check if protected user
                if ($ProtectedUsers -contains $principalId) {
                    Write-Host " └─ 🛡️ Protected user - skipping" -ForegroundColor Yellow
                    $script:protectedCounter++
                    continue
                }

                # Check if protected role
                if ($script:protectedRoles -contains $roleName) {
                    Write-Host " └─ ⚠️ Protected role - skipping" -ForegroundColor Yellow
                    $script:protectedCounter++
                    continue
                }

                # Check if assignment is inherited
                $isInherited = $false
                $inheritedReason = ""

                if ($assignment.PSObject.Properties.Name -contains "memberType" -and $assignment.memberType -eq "Inherited") {
                    $isInherited = $true
                    $inheritedReason = "memberType=Inherited"
                }
                elseif ($assignment.PSObject.Properties.Name -contains "ScopeType" -and $assignment.ScopeType -eq "managementgroup") {
                    $isInherited = $true
                    $inheritedReason = "ScopeType=managementgroup"
                }
                elseif ($assignment.PSObject.Properties.Name -contains "ScopeId" -and $assignment.ScopeId -like "*managementGroups*") {
                    $isInherited = $true
                    $inheritedReason = "ScopeId contains managementGroups"
                }

                if ($isInherited) {
                    Write-Host " └─ ⏭️ Inherited assignment ($inheritedReason) - skipping" -ForegroundColor DarkYellow
                    $script:skipCounter++
                    continue
                }

                # Remove assignment
                Write-Host " └─ 🗑️ Not in config - removing..." -ForegroundColor Magenta
                if ($PSCmdlet.ShouldProcess("Remove $ResourceType assignment for $principalName with role '$roleName'")) {
                    try {
                        if ($config.GroupBased) {
                            # For groups, we need to use groupId and memberType
                            & $config.RemoveCmd -TenantId $ApiInfo.TenantId -GroupId $assignment.id.Split('_')[0] -PrincipalId $principalId -type $roleName
                        }
                        else {
                            # For Azure and Entra roles, use RoleName
                            $params = @{
                                TenantId = $ApiInfo.TenantId
                                PrincipalId = $principalId
                                RoleName = $roleName
                            }
                            if ($scope) {
                                $params.Scope = $scope
                            }
                            & $config.RemoveCmd @params
                        }

                        $script:removeCounter++
                        Write-Host " ✓ Removed successfully" -ForegroundColor Green
                    }
                    catch {
                        Write-Host " ❌ Failed to remove: $_" -ForegroundColor Red
                    }
                }
                else {
                    $script:skipCounter++
                    Write-Host " ⏭️ Removal skipped (WhatIf mode)" -ForegroundColor DarkYellow
                }
            }
        }
    }
    catch {
        Write-Error "An error occurred processing $ResourceType cleanup: $_"
    }

    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
    Write-Host "│ 🛡️ Protected: $script:protectedCounter" -ForegroundColor White
    Write-Host "└────────────────────────────────────────────────────┘" -ForegroundColor Cyan

    if ($KeptCounter) { $KeptCounter.Value = $script:keptCounter }
    if ($RemoveCounter) { $RemoveCounter.Value = $script:removeCounter }
    if ($SkipCounter) { $SkipCounter.Value = $script:skipCounter }

    return @{
        ResourceType = $ResourceType;
        KeptCount = $script:keptCounter;
        RemovedCount = $script:removeCounter;
        SkippedCount = $script:skipCounter;
        ProtectedCount = $script:protectedCounter
    }
}


# Define script-level counters at the top of the file (outside any function)
$script:keptCounter = 0
$script:removeCounter = 0
$script:skipCounter = 0
$script:protectedCounter = 0

# Define protected roles at script level
$script:protectedRoles = @(
    "User Access Administrator",
    "Global Administrator",
    "Privileged Role Administrator",
    "Security Administrator"
)

function Invoke-Cleanup {
    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([System.Collections.Hashtable])]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
    param (
        [string]$ResourceType,
        [array]$ConfigAssignments,
        [hashtable]$ApiInfo,
        [array]$ProtectedUsers,
        [Parameter(Mandatory = $false)]
        [ValidateSet('Initial', 'Delta')]
        [string]$Mode = 'Delta',
        [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
    $script:protectedCounter = 0

    #region Prevent duplicate calls
    if (-not $script:ProcessedCleanups) { $script:ProcessedCleanups = @{} }

    $uniqueKey = "$ResourceType-$Mode"
    if ($ApiInfo.GroupId) { $uniqueKey += "-$($ApiInfo.GroupId)" }

    if ($script:ProcessedCleanups.ContainsKey($uniqueKey)) {
        Write-Host "`n⚠️ Cleanup for '$ResourceType' ($Mode mode) already processed - skipping duplicate call`n" -ForegroundColor Yellow
        return @{
            ResourceType = $ResourceType;
            KeptCount = 0;
            RemovedCount = 0;
            SkippedCount = 0;
            ProtectedCount = 0
        }
    }

    $script:ProcessedCleanups[$uniqueKey] = (Get-Date)

    Write-Host "`n┌────────────────────────────────────────────────────┐" -ForegroundColor Cyan
    Write-Host "│ Processing $ResourceType $Mode Cleanup" -ForegroundColor Cyan
    Write-Host "└────────────────────────────────────────────────────┘`n" -ForegroundColor Cyan

    # Display initial warning for Initial mode
    if ($Mode -eq 'Initial') {
        Write-Host "`n┌────────────────────────────────────────────────────┐" -ForegroundColor Yellow
        Write-Host "│ ⚠️ CAUTION: POTENTIALLY DESTRUCTIVE OPERATION" -ForegroundColor Yellow
        Write-Host "└────────────────────────────────────────────────────┘`n" -ForegroundColor Yellow
        Write-Host "This will remove ALL PIM assignments not defined in your configuration." -ForegroundColor Yellow
        Write-Host "If your protected users list is incomplete, you may lose access to critical resources!" -ForegroundColor Yellow
        Write-Host "Protected users count: $($ProtectedUsers.Count)" -ForegroundColor Yellow
        Write-Host "`n---" -ForegroundColor Yellow
        Write-Host "USAGE GUIDANCE:" -ForegroundColor Yellow
        Write-Host "• To preview changes without making them: Use -WhatIf" -ForegroundColor Yellow
        Write-Host "• To skip confirmation prompts: Use -Confirm:`$false" -ForegroundColor Yellow
        Write-Host "---`n" -ForegroundColor Yellow

        # Global confirmation for Initial mode
        $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
        }
    }

    # Define resource type specific settings
    $config = switch ($ResourceType) {
        "Azure Role eligible" {
            @{
                ApiEndpoint = "/providers/Microsoft.Authorization/roleEligibilityScheduleRequests";
                ApiVersion = "2020-10-01";
                RemoveCmd = "Remove-PIMAzureResourceEligibleAssignment";
                Filter = "status eq 'Provisioned'"
            }
        }
        "Azure Role active" {
            @{
                ApiEndpoint = "/providers/Microsoft.Authorization/roleAssignmentScheduleRequests";
                ApiVersion = "2020-10-01";
                RemoveCmd = "Remove-PIMAzureResourceActiveAssignment";
                Filter = "status eq 'Provisioned'"
            }
        }
        "Entra Role eligible" {
            @{
                ApiEndpoint = "https://graph.microsoft.com/beta/roleManagement/directory/roleEligibilityScheduleInstances"
                ApiVersion = "beta"
                RemoveCmd = "Remove-PIMEntraRoleEligibleAssignment"
                GraphBased = $true
            }
        }
        "Entra Role active" {
            @{
                ApiEndpoint = "https://graph.microsoft.com/beta/roleManagement/directory/roleAssignmentScheduleInstances"
                ApiVersion = "beta"
                RemoveCmd = "Remove-PIMEntraRoleActiveAssignment"
                GraphBased = $true
            }
        }
        "Group eligible" {
            @{
                ApiEndpoint = "https://graph.microsoft.com/beta/roleManagement/directory/roleEligibilityScheduleInstances"
                ApiVersion = "beta"
                RemoveCmd = "Remove-PIMGroupEligibleAssignment"
                GraphBased = $true
                GroupBased = $true
            }
        }
        "Group active" {
            @{
                ApiEndpoint = "https://graph.microsoft.com/beta/roleManagement/directory/roleAssignmentScheduleInstances"
                ApiVersion = "beta"
                RemoveCmd = "Remove-PIMGroupActiveAssignment"
                GraphBased = $true
                GroupBased = $true
            }
        }
        default {
            throw "Unknown resource type: $ResourceType"
        }
    }

    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 ==========="


    try {
        # Get current assignments directly using scopes from config
        Write-Host "`n=== Processing Scopes ===" -ForegroundColor Cyan
        $allAssignments = @()

        if ($config.GraphBased) {
            if ($config.GroupBased) {
                # For group assignments, need to get assignments for each group
                Write-Host " 🔍 Checking group assignments" -ForegroundColor White
                try {
                    $getCmd = if ($ResourceType -eq "Group eligible") {
                        "Get-PIMGroupEligibleAssignment"
                    } else {
                        "Get-PIMGroupActiveAssignment"
                    }

                    foreach ($groupId in $ApiInfo.GroupIds) {
                        Write-Host " ├─ Processing group: $groupId" -ForegroundColor White
                        $groupAssignments = & $getCmd -TenantId $ApiInfo.TenantId -groupId $groupId
                        if ($null -ne $groupAssignments) {
                            $allAssignments += $groupAssignments
                            Write-Host " │ └─ Found $($groupAssignments.Count) assignments" -ForegroundColor Gray
                        }
                    }
                }
                catch {
                    Write-Error "Failed to get group assignments: $_"
                }
            }
            else {
                # For Entra roles, get assignments using our module's commands
                Write-Host " 🔍 Checking tenant-wide Entra Role assignments" -ForegroundColor White
                try {
                    $getCmd = if ($ResourceType -eq "Entra Role eligible") {
                        "Get-PIMEntraRoleEligibleAssignment"
                    } else {
                        "Get-PIMEntraRoleActiveAssignment"
                    }

                    $allAssignments = & $getCmd -TenantId $ApiInfo.TenantId
                    Write-Host " ├─ Found $($allAssignments.Count) assignments" -ForegroundColor Gray
                }
                catch {
                    Write-Error "Failed to get Entra role assignments: $_"
                }
            }
        }
        else {
            # For Azure roles, use existing scope-based logic
            $getCmd = if ($ResourceType -eq "Azure Role eligible") {
                "Get-PIMAzureResourceEligibleAssignment"
            } else {
                "Get-PIMAzureResourceActiveAssignment"
            }

            # Track processed scopes to avoid duplicates
            $processedScopes = @{}

            foreach ($configAssignment in $ConfigAssignments) {
                if ($configAssignment.Scope) {
                    # Skip if we've already processed this scope
                    if ($processedScopes.ContainsKey($configAssignment.Scope)) {
                        Write-Verbose "Skipping already processed scope: $($configAssignment.Scope)"
                        continue
                    }

                    Write-Host " 🔍 Checking scope: $($configAssignment.Scope)" -ForegroundColor White
                    $params = @{
                        TenantId = $ApiInfo.TenantId
                        Scope = $configAssignment.Scope
                    }

                    $scopeAssignments = & $getCmd @params
                    if ($null -ne $scopeAssignments) {
                        $allAssignments += $scopeAssignments
                    }

                    Write-Host " ├─ Found $($scopeAssignments.Count) assignments" -ForegroundColor Gray
                    $processedScopes[$configAssignment.Scope] = $true
                }
            }
        }

        Write-Host "`n=== Processing Assignments ===" -ForegroundColor Cyan
        Write-Host " 📊 Total assignments found: $($allAssignments.Count)" -ForegroundColor White

        # Create a tracking set for processed assignments to avoid duplicates
        $processedAssignments = @{}

        # Process assignments
        if ($allAssignments.Count -gt 0) {
            foreach ($assignment in $allAssignments) {
                # Extract assignment details with proper fallbacks for each property
                $principalId = if ($config.GraphBased) {
                    $assignment.principalid  # Our module's commands provide this consistently
                }
                elseif ($null -ne $assignment.PrincipalId) {
                    $assignment.PrincipalId
                }
                elseif ($null -ne $assignment.principalId) {
                    $assignment.principalId
                }
                else {
                    $null
                }

                # Handle role name/member type based on resource type
                $roleName = if ($config.GroupBased) {
                    $assignment.memberType  # For groups, use memberType (member/owner)
                }
                elseif ($config.GraphBased) {
                    $assignment.rolename  # For Entra roles, use rolename consistently
                }
                elseif ($null -ne $assignment.RoleName -and $assignment.RoleName -ne '') {
                    $assignment.RoleName
                }
                elseif ($null -ne $assignment.roleName -and $assignment.roleName -ne '') {
                    $assignment.roleName
                }
                elseif ($null -ne $assignment.RoleDefinitionDisplayName -and $assignment.RoleDefinitionDisplayName -ne '') {
                    $assignment.RoleDefinitionDisplayName
                }
                else {
                    $roleId = $assignment.RoleId
                    if ($roleId) {
                        try {
                            $roleDefinition = Get-AzRoleDefinition -Id ($roleId -split '/')[-1]
                            if ($roleDefinition) {
                                $roleDefinition.Name
                            }
                            else { $null }
                        }
                        catch {
                            Write-Verbose "Failed to get role name from role definition: $_"
                            $null
                        }
                    }
                    else { $null }
                }

                # Get scope - handle Azure roles properly
                $scope = if ($config.GraphBased) {
                    # For Entra roles, try to get the directoryScopeId for AU-scoped assignments
                    if ($assignment.PSObject.Properties.Name -contains 'directoryScopeId') {
                        $assignment.directoryScopeId
                    } else {
                        $null  # Default for tenant-wide Entra roles
                    }
                }
                else {  # For Azure roles, always use ScopeId
                    $assignment.ScopeId
                }

                # Get principal name - simplified since our module provides consistent output
                $principalName = if ($config.GraphBased) {
                    $assignment.principalname  # Our module's commands provide this consistently
                }
                elseif ($null -ne $assignment.PrincipalName) {
                    $assignment.PrincipalName
                }
                elseif ($null -ne $assignment.principalName) {
                    $assignment.principalName
                }
                else {
                    "Principal-$principalId"
                }

                Write-Host "`n Processing: $principalName" -ForegroundColor White
                Write-Host " ├─ Role: $roleName" -ForegroundColor Gray
                if ($scope) {
                    Write-Host " ├─ Scope: $scope" -ForegroundColor Gray
                }

                # Skip invalid assignments
                if (-not $principalId -or -not $roleName) {
                    Write-Host " └─ ⚠️ Invalid assignment data (missing principalId or roleName) - skipping" -ForegroundColor Yellow
                    $script:skipCounter++
                    continue
                }
                # For non-Graph based assignments (Azure roles), require scope
                if (-not $config.GraphBased -and -not $scope) {
                    Write-Host " └─ ⚠️ Invalid assignment data (missing scope for Azure role) - skipping" -ForegroundColor Yellow
                    $script:skipCounter++
                    continue
                }

                # Create a unique key to track this assignment - for Graph-based assignments, don't include scope
                $assignmentKey = if ($config.GraphBased) {
                    "$principalId|$roleName"
                } else {
                    "$principalId|$roleName|$scope"
                }

                # Skip if we've already processed this assignment
                if ($processedAssignments.ContainsKey($assignmentKey)) {
                    Write-Host " └─ ⏭️ Duplicate entry - skipping" -ForegroundColor DarkYellow
                    $script:skipCounter++
                    continue
                }

                # Mark as processed
                $processedAssignments[$assignmentKey] = $true

                # Check if assignment matches config
                $foundInConfig = $false
                foreach ($configAssignment in $ConfigAssignments) {
                    $matchesPrincipal = $configAssignment.PrincipalId -eq $principalId
                    $matchesRole = $configAssignment.RoleName -ieq $roleName

                    # For Graph-based assignments (Entra Roles), ignore scope comparison
                    $matchesScope = if ($config.GraphBased) {
                        $true
                    } else {
                        $configAssignment.Scope -eq $scope
                    }

                    if ($matchesPrincipal -and $matchesRole -and $matchesScope) {
                        $foundInConfig = $true
                        break
                    }
                }

                # Keep assignment if it's in config
                if ($foundInConfig) {
                    Write-Host " └─ ✅ Matches config - keeping" -ForegroundColor Green
                    $script:keptCounter++
                    continue
                }

                # Check if protected user
                if ($ProtectedUsers -contains $principalId) {
                    Write-Host " └─ 🛡️ Protected user - skipping" -ForegroundColor Yellow
                    $script:protectedCounter++
                    continue
                }

                # Check if protected role
                if ($script:protectedRoles -contains $roleName) {
                    Write-Host " └─ ⚠️ Protected role - skipping" -ForegroundColor Yellow
                    $script:protectedCounter++
                    continue
                }

                # Check if assignment is inherited
                $isInherited = $false
                $inheritedReason = ""

                if ($assignment.PSObject.Properties.Name -contains "memberType" -and $assignment.memberType -eq "Inherited") {
                    $isInherited = $true
                    $inheritedReason = "memberType=Inherited"
                }
                elseif ($assignment.PSObject.Properties.Name -contains "ScopeType" -and $assignment.ScopeType -eq "managementgroup") {
                    $isInherited = $true
                    $inheritedReason = "ScopeType=managementgroup"
                }
                elseif ($assignment.PSObject.Properties.Name -contains "ScopeId" -and $assignment.ScopeId -like "*managementGroups*") {
                    $isInherited = $true
                    $inheritedReason = "ScopeId contains managementGroups"
                }

                if ($isInherited) {
                    Write-Host " └─ ⏭️ Inherited assignment ($inheritedReason) - skipping" -ForegroundColor DarkYellow
                    $script:skipCounter++
                    continue
                }

                # For initial mode, we remove everything not in config
                if ($Mode -eq "Initial") {
                    Write-Host " └─ 🗑️ Not in config - removing..." -ForegroundColor Magenta
                } else {
                    # For delta mode, only remove if it was created by the orchestrator
                    # Use direct ARM/Graph API calls to check the schedule requests for justification
                    # Create parameters hash table for Test-AssignmentCreatedByOrchestrator
                    $testParams = @{
                        Assignment = $assignment
                        TenantId = $ApiInfo.TenantId
                        ResourceType = $ResourceType
                    }

                    # Only add SubscriptionId if it exists in ApiInfo (it won't for Entra roles)
                    if ($ApiInfo.ContainsKey('SubscriptionId')) {
                        $testParams['SubscriptionId'] = $ApiInfo.SubscriptionId
                    }

                    # Call the function with splatted parameters
                    $isFromOrchestrator = Test-AssignmentCreatedByOrchestrator @testParams -Verbose:$VerbosePreference

                    if (-not $isFromOrchestrator) {
                        Write-Host " └─ ⏭️ Not created by orchestrator - skipping (delta mode)" -ForegroundColor DarkYellow
                        $script:skipCounter++
                        continue
                    } else {
                        Write-Host " └─ 🔍 Created by orchestrator but not in config - removing..." -ForegroundColor Magenta
                    }
                }

                if ($PSCmdlet.ShouldProcess("Remove $ResourceType assignment for $principalName with role '$roleName'")) {
                    try {
                        if ($config.GroupBased) {
                            # For groups, we need to use groupId and memberType
                            & $config.RemoveCmd -TenantId $ApiInfo.TenantId -GroupId $assignment.id.Split('_')[0] -PrincipalId $principalId -type $roleName
                        }
                        else {
                            # For Azure and Entra roles, use RoleName
                            $params = @{
                                TenantId = $ApiInfo.TenantId
                                PrincipalId = $principalId
                                RoleName = $roleName
                            }

                            # For Entra roles with Administrative Unit scope
                            if ($config.GraphBased -and $scope -and $scope -like "/administrativeUnits/*") {
                                $auId = ($scope -split '/')[-1]
                                Write-Host " ⚠️ Administrative Unit scoped assignments are not currently supported for removal" -ForegroundColor Yellow
                                Write-Host " ℹ️ Administrative Unit ID: $auId" -ForegroundColor Cyan
                                Write-Host " ℹ️ Full scope: $scope" -ForegroundColor Cyan
                                Write-Host " ⏭️ Skipping removal" -ForegroundColor Yellow
                                $script:skipCounter++
                                continue
                            }
                            # For Azure resources with scope
                            elseif ($scope) {
                                $params.Scope = $scope
                            }

                            & $config.RemoveCmd @params
                        }

                        $script:removeCounter++
                        Write-Host " ✓ Removed successfully" -ForegroundColor Green
                    }
                    catch {
                        Write-Host " ❌ Failed to remove: $_" -ForegroundColor Red
                    }
                }
                else {
                    $script:skipCounter++
                    Write-Host " ⏭️ Removal skipped (WhatIf mode)" -ForegroundColor DarkYellow
                }
            }
        }
    }
    catch {
        Write-Error "An error occurred processing $ResourceType cleanup: $_"
    }

    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
    Write-Host "│ 🛡️ Protected: $script:protectedCounter" -ForegroundColor White
    Write-Host "└────────────────────────────────────────────────────┘" -ForegroundColor Cyan

    if ($KeptCounter) { $KeptCounter.Value = $script:keptCounter }
    if ($RemoveCounter) { $RemoveCounter.Value = $script:removeCounter }
    if ($SkipCounter) { $SkipCounter.Value = $script:skipCounter }

    return @{
        ResourceType = $ResourceType;
        KeptCount = $script:keptCounter;
        RemovedCount = $script:removeCounter;
        SkippedCount = $script:skipCounter;
        ProtectedCount = $script:protectedCounter
    }
}


function Invoke-EasyPIMCleanup {
    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([System.Collections.Hashtable])]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
    param (
        [Parameter(Mandatory = $true)]
        [object]$Config,

        [Parameter(Mandatory = $true)]
        [string]$TenantId,

        [Parameter(Mandatory = $true)]
        [string]$SubscriptionId,

        [Parameter(Mandatory = $false)]
        [ValidateSet("initial", "delta")]
        [string]$Mode = "delta"
    )

    # Confirm the operation with the user
    $operationTarget = "PIM assignments across Azure, Entra ID, and Groups"
    $operationDescription = "$Mode mode cleanup - process assignments according to configuration"
    if (-not $PSCmdlet.ShouldProcess($operationTarget, $operationDescription)) {
        Write-Output "Operation cancelled by user."
        return @{
            KeptCount = 0
            RemovedCount = 0
            SkippedCount = 0
            ProtectedCount = 0
        }
    }

    $results = @()

    # Process Azure Resource roles (eligible)
    if ($Config.AzureRoles) {
        $apiInfo = @{
            TenantId = $TenantId
            SubscriptionId = $SubscriptionId
        }
        $results += Invoke-Cleanup -ResourceType "Azure Role eligible" -ConfigAssignments $Config.AzureRoles -ApiInfo $apiInfo -ProtectedUsers $Config.ProtectedUsers -Mode $Mode
    }

    # Process Azure Resource roles (active)
    if ($Config.AzureRolesActive) {
        $apiInfo = @{
            TenantId = $TenantId
            SubscriptionId = $SubscriptionId
        }
        $results += Invoke-Cleanup -ResourceType "Azure Role active" -ConfigAssignments $Config.AzureRolesActive -ApiInfo $apiInfo -ProtectedUsers $Config.ProtectedUsers -Mode $Mode
    }

    # Process Entra ID roles (eligible)
    if ($Config.EntraIDRoles) {
        $apiInfo = @{
            TenantId = $TenantId
        }
        $results += Invoke-Cleanup -ResourceType "Entra Role eligible" -ConfigAssignments $Config.EntraIDRoles -ApiInfo $apiInfo -ProtectedUsers $Config.ProtectedUsers -Mode $Mode
    }

    # Process Entra ID roles (active)
    if ($Config.EntraIDRolesActive) {
        $apiInfo = @{
            TenantId = $TenantId
        }
        $results += Invoke-Cleanup -ResourceType "Entra Role active" -ConfigAssignments $Config.EntraIDRolesActive -ApiInfo $apiInfo -ProtectedUsers $Config.ProtectedUsers -Mode $Mode
    }

    # Process Group roles (eligible)
    if ($Config.GroupRoles) {
        foreach ($groupConfig in $Config.GroupRoles) {
            if ($groupConfig.GroupId) {
                $apiInfo = @{
                    TenantId = $TenantId
                    GroupIds = @($groupConfig.GroupId)
                }
                $results += Invoke-Cleanup -ResourceType "Group eligible" -ConfigAssignments $Config.GroupRoles -ApiInfo $apiInfo -ProtectedUsers $Config.ProtectedUsers -Mode $Mode
            }
        }
    }

    # Process Group roles (active)
    if ($Config.GroupRolesActive) {
        foreach ($groupConfig in $Config.GroupRolesActive) {
            if ($groupConfig.GroupId) {
                $apiInfo = @{
                    TenantId = $TenantId
                    GroupIds = @($groupConfig.GroupId)
                }
                $results += Invoke-Cleanup -ResourceType "Group active" -ConfigAssignments $Config.GroupRolesActive -ApiInfo $apiInfo -ProtectedUsers $Config.ProtectedUsers -Mode $Mode
            }
        }
    }

    # Aggregate results
    $totalKept = ($results | Measure-Object -Property KeptCount -Sum).Sum
    $totalRemoved = ($results | Measure-Object -Property RemovedCount -Sum).Sum
    $totalSkipped = ($results | Measure-Object -Property SkippedCount -Sum).Sum
    $totalProtected = ($results | Measure-Object -Property ProtectedCount -Sum).Sum

    return @{
        KeptCount = $totalKept
        RemovedCount = $totalRemoved
        SkippedCount = $totalSkipped
        ProtectedCount = $totalProtected
    }
}

function Invoke-ResourceAssignment {
    [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
        }

        if (-not $roleName) {
            Write-Warning "Assignment is missing Role/Rolename: $($assignment | ConvertTo-Json -Compress)"
            $errorCounter++
            continue
        }

        # Get a friendly name for the principal
        $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*") {
                # For Azure roles, use lowercase property names
                $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 Entra ID and Azure role assignments
                    # 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 without writing the summary (it will be handled by EPO_New-Assignment.ps1)
    return @{
        Created = $createCounter
        Skipped = $skipCounter
        Failed  = $errorCounter
    }
}

# Create an alias for backward compatibility
Set-Alias -Name Invoke-ResourceAssignments -Value Invoke-ResourceAssignment


# Contains output formatting functions for the EasyPIM module
# These functions are meant to be used for terminal display, so Write-Host is appropriate here


function Write-SectionHeader {
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
    param (
        [Parameter(Mandatory = $true)]
        [string]$Message
    )

    Write-Host "`n┌────────────────────────────────────────────────────┐" -ForegroundColor Cyan
    Write-Host "│ $Message" -ForegroundColor Cyan
    Write-Host "└────────────────────────────────────────────────────┘`n" -ForegroundColor Cyan
}

function Write-SubHeader {
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
    param (
        [Parameter(Mandatory = $true)]
        [string]$Message
    )

    Write-Host "`n=== $Message ===" -ForegroundColor Cyan
}

function Write-StatusInfo {
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
    param (
        [Parameter(Mandatory = $true)]
        [string]$Message
    )

    Write-Host " ℹ️ $Message" -ForegroundColor White
}

function Write-StatusSuccess {
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
    param (
        [Parameter(Mandatory = $true)]
        [string]$Message
    )

    Write-Host " ✅ $Message" -ForegroundColor Green
}

function Write-StatusWarning {
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
    param (
        [Parameter(Mandatory = $true)]
        [string]$Message
    )

    Write-Host " ⚠️ $Message" -ForegroundColor Yellow
}

function Write-StatusError {
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
    param (
        [Parameter(Mandatory = $true)]
        [string]$Message
    )

    Write-Host " ❌ $Message" -ForegroundColor Red
}

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┌────────────────────────────────────────────────────┐" -ForegroundColor Cyan
    Write-Host "│ $Category Summary" -ForegroundColor Cyan
    Write-Host "├────────────────────────────────────────────────────┤" -ForegroundColor Cyan
    Write-Host "│ ✅ Kept: $Kept" -ForegroundColor White
    Write-Host "│ 🗑️ Removed: $Removed" -ForegroundColor White
    Write-Host "│ 🛡️ Protected: $Protected" -ForegroundColor White
    Write-Host "└────────────────────────────────────────────────────┘" -ForegroundColor Cyan
}

function Write-Summary {
    [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┌───────────────────────────────────────────────────────────────────────────────┐" -ForegroundColor Cyan
    Write-Host "│ SUMMARY: $Category" -ForegroundColor Cyan
    Write-Host "├───────────────────────────────────────────────────────────────────────────────┤" -ForegroundColor Cyan
    Write-Host "│ ✅ Created : $Created" -ForegroundColor White
    Write-Host "│ ⏭️ Skipped : $Skipped" -ForegroundColor White
    Write-Host "│ ❌ Failed : $Failed" -ForegroundColor White
    Write-Host "└───────────────────────────────────────────────────────────────────────────────┘" -ForegroundColor Cyan
}



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-ResourceAssignment -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-ResourceAssignment -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-ResourceAssignment -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-ResourceAssignment -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-ResourceAssignment -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

        # Use Write-StatusInfo instead of Write-GroupHeader
        Write-StatusInfo "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-ResourceAssignment -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
}

# This function is deprecated.
# The functionality has been consolidated into:
# - Invoke-DeltaCleanup (for cleanup operations)
# - Invoke-PIMAssignment (for creation operations)
# This function can be safely removed in a future version.
function Invoke-PIMAssignments {
    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([System.Collections.Hashtable])]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet("Create", "Remove")]
        [string]$Operation,

        [Parameter(Mandatory = $true)]
        [string]$ResourceType,

        [Parameter(Mandatory = $true)]
        [array]$Assignments,

        [Parameter(Mandatory = $true)]
        [array]$ConfigAssignments,

        [Parameter(Mandatory = $true)]
        [hashtable]$CommandMap,

        [Parameter(Mandatory = $false)]
        [array]$ProtectedUsers = @(),

        [Parameter(Mandatory = $false)]
        [string]$CleanupMode = "delta",

        [Parameter(Mandatory = $false)]
        [switch]$FilterByJustification,

        [Parameter(Mandatory = $false)]
        [string]$JustificationFilter = "Invoke-EasyPIMOrchestrator"
    )

    # Determine operation type
    $isCreationOperation = $Operation -eq "Create"
    $isRemovalOperation = $Operation -eq "Remove"

    # Initialize counters based on operation type
    if ($isCreationOperation) {
        $createCounter = 0
        $skipCounter = 0
        $errorCounter = 0
    } else {
        $keptCounter = 0
        $removeCounter = 0
        $skipCounter = 0
        $protectedCounter = 0
    }

    # Create a tracking map for processed assignments
    $processedAssignments = @{}

    # Get resource type category for matching logic
    $resourceTypeCategory = if ($ResourceType -like "Azure*") {
        "Azure"
    } elseif ($ResourceType -like "Entra*") {
        "Entra"
    } elseif ($ResourceType -like "Group*") {
        "Group"
    } else {
        "Unknown"
    }

    # Determine group ID if applicable
    $groupId = $null
    if ($resourceTypeCategory -eq "Group") {
        if ($isRemovalOperation) {
            if ($CommandMap.ContainsKey('GroupIds') -and $CommandMap.GroupIds.Count -gt 0) {
                $groupId = $CommandMap.GroupIds[0]
                Write-Verbose "Using group ID $groupId from CommandMap for removal operation"
            }
            elseif ($CommandMap.ContainsKey('groupId') -and -not [string]::IsNullOrEmpty($CommandMap.groupId)) {
                $groupId = $CommandMap.groupId
                Write-Verbose "Using group ID $groupId from CommandMap (singular key) for removal operation"
            }
            elseif ($ResourceType -like "*active*" -and $CommandMap.ContainsKey('ActiveGroupId')) {
                $groupId = $CommandMap.ActiveGroupId
                Write-Verbose "Using ActiveGroupId $groupId from CommandMap for removal operation"
            }
            elseif ($ResourceType -like "*eligible*" -and $CommandMap.ContainsKey('EligibleGroupId')) {
                $groupId = $CommandMap.EligibleGroupId
                Write-Verbose "Using EligibleGroupId $groupId from CommandMap for removal operation"
            }

            if (-not $groupId -and $Assignments.Count -gt 0) {
                foreach ($assignment in $Assignments) {
                    if ($assignment.PSObject.Properties.Name -contains "groupId" -and $assignment.groupId) {
                        $groupId = $assignment.groupId
                        Write-Verbose "Found groupId $groupId in assignment data for removal operation"
                        break
                    }
                }
            }
        }
        elseif ($isCreationOperation -and $Assignments.Count -gt 0) {
            $firstAssignment = $Assignments[0]
            if ($firstAssignment.PSObject.Properties.Name -contains "GroupId") {
                $groupId = $firstAssignment.GroupId
                Write-Verbose "Using group ID $groupId from assignments for creation operation"
            }
        }
    }

    # Create simpler header text without box formatting
    Write-Host "`n=== Processing Assignments ===" -ForegroundColor Cyan
    Write-Host " 📊 Total assignments found: $($Assignments.Count)" -ForegroundColor White

    # Get existing assignments
    $existingAssignments = @()
    try {
        if ($isCreationOperation) {
            $cmd = $CommandMap.GetCmd
            $params = $CommandMap.GetParams
        } else {
            $cmd = $CommandMap.GetCommand
            $params = @{
                tenantID = $CommandMap.TenantId
            }

            if ($resourceTypeCategory -eq "Azure" -and $CommandMap.Subscriptions -and $CommandMap.Subscriptions.Count -gt 0) {
                $params.subscriptionID = $CommandMap.Subscriptions[0]
            }

            if ($resourceTypeCategory -eq "Group" -and $groupId) {
                $params.groupId = $groupId
            }
        }

        if ($resourceTypeCategory -eq "Group") {
            if ($groupId) {
                $cmdExpression = "$cmd -tenantID '$($CommandMap.TenantId)' -groupID '$groupId'"
                $scriptBlock = [ScriptBlock]::Create($cmdExpression)
                $existingAssignments = Invoke-Command -ScriptBlock $scriptBlock -ErrorAction SilentlyContinue
            } else {
                Write-Warning "No group ID available for Group resource, skipping assignment retrieval"
                $existingAssignments = @()
            }
        } else {
            $existingAssignments = & $cmd @params
        }

        Write-Host " 🔍 Analyzing configuration" -ForegroundColor Cyan
        Write-Host " ├─ Found $($Assignments.Count) assignments to process" -ForegroundColor White
        Write-Host " └─ Found $($existingAssignments.Count) existing assignments" -ForegroundColor White
    }
    catch {
        Write-Host " └─ ⚠️ Error fetching existing assignments: $_" -ForegroundColor Yellow
        $existingAssignments = @()
    }

    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)
    }

    Write-Host "`n 📋 Processing assignments:" -ForegroundColor Cyan

    if ($Assignments.Count -eq 0) {
        Write-Host " ├─ No assignments to process" -ForegroundColor White

        if ($isCreationOperation) {
            return @{
                Created = 0
                Skipped = 0
                Failed = 0
            }
        } else {
            return @{
                ResourceType = $ResourceType
                KeptCount = 0
                RemovedCount = 0
                SkippedCount = 0
                ProtectedCount = 0
            }
        }
    }

    foreach ($assignment in $Assignments) {
        $props = Get-AssignmentProperties -Assignment $assignment
        $principalId = $props.PrincipalId
        $roleName = $props.RoleName
        $principalName = $props.PrincipalName
        $scope = $props.Scope

        Write-Host "`n Processing: $principalName" -ForegroundColor White
        Write-Host " ├─ Role: $roleName" -ForegroundColor Gray
        if ($scope) {
            Write-Host " ├─ Scope: $scope" -ForegroundColor Gray
        }

        $currentGroupId = $null
        if ($resourceTypeCategory -eq "Group") {
            if ($isCreationOperation) {
                if ($assignment.PSObject.Properties.Name -contains "GroupId") {
                    $currentGroupId = $assignment.GroupId
                    Write-Verbose "Found GroupId $currentGroupId in assignment for creation"
                }
            } else {
                $currentGroupId = $groupId
                Write-Verbose "Using GroupId $currentGroupId from CommandMap for removal"
            }
        }

        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)"
            $skipCounter++
            continue
        }

        $assignmentKey = "$principalId|$roleName"
        if ($scope) { $assignmentKey += "|$scope" }
        if ($currentGroupId) { $assignmentKey += "|$currentGroupId" }

        if ($processedAssignments.ContainsKey($assignmentKey)) {
            Write-Host " ├─ ⏭️ $principalName with role '$roleName' is a duplicate entry, skipping" -ForegroundColor DarkYellow
            $skipCounter++
            continue
        }

        $processedAssignments[$assignmentKey] = $true

        if (-not (Test-PrincipalExists -PrincipalId $principalId)) {
            Write-Host " │ ❌ Principal '$principalName' ($principalId) does not exist, skipping assignment" -ForegroundColor Red
            $errorCounter++
            continue
        }

        if ($resourceTypeCategory -eq "Group") {
            if (-not $currentGroupId) {
                Write-Host " ├─ ❌ Missing GroupId for assignment, skipping" -ForegroundColor Red
                $errorCounter++
                continue
            }

            try {
                $uri = "https://graph.microsoft.com/v1.0/directoryObjects/$currentGroupId"
                Invoke-MgGraphRequest -Uri $uri -Method GET -ErrorAction Stop
                Write-Verbose "Group $currentGroupId exists and is accessible"

                if (-not (Test-GroupEligibleForPIM -GroupId $currentGroupId)) {
                    Write-Host " │ ⚠️ Group $currentGroupId is not eligible for PIM management (likely synced from on-premises), skipping" -ForegroundColor Yellow
                    $skipCounter++
                    continue
                }
            }
            catch {
                Write-Host " │ ⚠️ Group $currentGroupId does not exist or cannot be accessed, skipping" -ForegroundColor Yellow
                $skipCounter++
                continue
            }
        }

        $scopeDisplay = ""
        if ($resourceTypeCategory -eq "Azure" -and $scope) {
            $scopeDisplay = " on scope $scope"
        } elseif ($resourceTypeCategory -eq "Group" -and $currentGroupId) {
            $scopeDisplay = " in group $currentGroupId"
        }

        Write-Host " ├─ 🔍 $principalName with role '$roleName'$scopeDisplay" -ForegroundColor White

        if ($isRemovalOperation -and $CleanupMode -eq "delta" -and $FilterByJustification) {
            $isFromOrchestrator = Test-IsJustificationFromOrchestrator -Assignment $assignment -JustificationFilter $JustificationFilter

            if (-not $isFromOrchestrator) {
                Write-Host " │ └─ ⏭️ Not created by orchestrator, skipping" -ForegroundColor DarkYellow
                $skipCounter++
                continue
            }
        }

        $foundInConfig = $false

        if ($isCreationOperation) {
            $foundInConfig = $true
        } else {
            $foundInConfig = Test-AssignmentInConfig -PrincipalId $principalId -RoleName $roleName `
                -Scope $scope -GroupId $currentGroupId -ConfigAssignments $ConfigAssignments -ResourceType $resourceTypeCategory
        }

        $existingAssignment = $null
        $matchInfo = ""

        foreach ($existing in $existingAssignments) {
            if ($resourceTypeCategory -eq "Entra") {
                $principalMatched = $false
                $roleMatched = $false

                if ($existing.PSObject.Properties.Name -contains 'principal') {
                    $principalMatched = $existing.principal.id -eq $principalId
                    $roleMatched = $existing.roleDefinition.displayName -ieq $roleName
                    if ($principalMatched -and $roleMatched) {
                        $matchInfo = "principal.id='$($existing.principal.id)' and roleDefinition.displayName='$($existing.roleDefinition.displayName)'"
                    }
                } else {
                    $principalMatched = $existing.PrincipalId -eq $principalId
                    $roleMatched = $existing.RoleName -ieq $roleName
                    if ($principalMatched -and $roleMatched) {
                        $matchInfo = "PrincipalId='$($existing.PrincipalId)' and RoleName='$($existing.RoleName)'"
                    }
                }

                if ($principalMatched -and $roleMatched) {
                    $existingAssignment = $existing
                    break
                }
            }
            elseif ($resourceTypeCategory -eq "Group") {
                $principalMatched = $false
                $roleMatched = $false

                if ($existing.PrincipalId -eq $principalId -or $existing.principalid -eq $principalId) {
                    $principalMatched = $true
                }

                if ($existing.RoleName -ieq $roleName -or $existing.Type -ieq $roleName -or $existing.memberType -ieq $roleName) {
                    $roleMatched = $true
                }

                if ($principalMatched -and $roleMatched) {
                    $matchInfo = if ($null -ne $existing.memberType) {
                        "memberType='$($existing.memberType)'"
                    }
                    elseif ($null -ne $existing.Type) {
                        "type='$($existing.Type)'"
                    }
                    else {
                        "role matched"
                    }
                    $matchInfo = "principalId='$($existing.principalId)' and $matchInfo"
                    $existingAssignment = $existing
                    break
                }
            }
            else {
                if (($existing.PrincipalId -eq $principalId) -and ($existing.RoleName -eq $roleName)) {
                    if ($scope) {
                        if ($existing.ScopeId -eq $scope) {
                            $matchInfo = "PrincipalId='$principalId', RoleName='$roleName', Scope='$scope'"
                            $existingAssignment = $existing
                            break
                        }
                    } else {
                        $matchInfo = "PrincipalId='$principalId', RoleName='$roleName'"
                        $existingAssignment = $existing
                        break
                    }
                }
            }
        }

        # Check for inherited assignments
        if ($existingAssignment -and
            (($existingAssignment.memberType -eq "Inherited") -or
             ($existingAssignment.ScopeType -eq "managementgroup") -or
             ($existingAssignment.ScopeId -like "*managementGroups*"))) {
            Write-Host " └─ ⏭️ Inherited assignment (memberType=Inherited) - skipping" -ForegroundColor DarkYellow
            $skipCounter++
            continue
        }

        # Check if assignment matches configuration
        if ($foundInConfig) {
            Write-Host " └─ ✅ Matches config - keeping" -ForegroundColor Green
            $keptCounter++
        } else {
            if ($ProtectedUsers -contains $principalId) {
                Write-Host " └─ 🛡️ Protected user - skipping" -ForegroundColor Yellow
                $protectedCounter++
            } else {
                Write-Host " └─ ❌ Not in config - removing" -ForegroundColor Red
                $removeCounter++
            }
        }
    }

    # End of processing assignments loop
    if ($isCreationOperation) {
        $summary = @{
            ResourceType = $ResourceType
            Created = [int]$createCounter
            Skipped = [int]$skipCounter
            Failed = [int]$errorCounter
        }

        if ($resourceTypeCategory -eq "Azure" -or $resourceTypeCategory -eq "Entra") {
            # For Azure and Entra roles, return summary without displaying it
            return $summary
        } else {
            # For other types (like Groups), write formatted summary and return
            $summaryOutput = Get-FormattedCleanupSummary -ResourceType "$ResourceType Assignments" `
                -KeptCount $summary.Created `
                -RemovedCount $summary.Failed `
                -SkippedCount $summary.Skipped
            Write-Host $summaryOutput
            return $summary
        }
    } else {
        $summary = @{
            ResourceType = $ResourceType
            KeptCount = [int]$keptCounter
            RemovedCount = [int]$removeCounter
            SkippedCount = [int]$skipCounter
            ProtectedCount = [int]$protectedCounter
        }

        if ($resourceTypeCategory -ne "Azure" -and $resourceTypeCategory -ne "Entra") {
            # For non-Azure/Entra types, write formatted summary
            $summaryOutput = Get-FormattedCleanupSummary -ResourceType "$ResourceType Cleanup" `
                -KeptCount $summary.KeptCount `
                -RemovedCount $summary.RemovedCount `
                -SkippedCount $summary.SkippedCount `
                -ProtectedCount $summary.ProtectedCount
            Write-Host $summaryOutput
        }
        return $summary
    }
}

# Process PIM assignments using the robust Invoke-ResourceAssignment function
function Invoke-PIMAssignment {
    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([System.Collections.Hashtable])]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "ConfigAssignments",
        Justification="Parameter is declared for API consistency but currently only used in the 'Create' operation path")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "CleanupMode",
        Justification="Parameter is declared for API consistency but currently only used in the 'Create' operation path")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "FilterByJustification",
        Justification="Parameter is declared for API consistency but currently only used in the 'Create' operation path")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "JustificationFilter",
        Justification="Parameter is declared for API consistency but currently only used in the 'Create' operation path")]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateSet("Create", "Remove")]
        [string]$Operation,

        [Parameter(Mandatory = $true)]
        [string]$ResourceType,

        [Parameter(Mandatory = $true)]
        [array]$Assignments,

        [Parameter(Mandatory = $true)]
        [array]$ConfigAssignments,

        [Parameter(Mandatory = $true)]
        [hashtable]$CommandMap,

        [Parameter(Mandatory = $false)]
        [array]$ProtectedUsers = @(),

        [Parameter(Mandatory = $false)]
        [string]$CleanupMode = "delta",

        [Parameter(Mandatory = $false)]
        [switch]$FilterByJustification,

        [Parameter(Mandatory = $false)]
        [string]$JustificationFilter = "Invoke-EasyPIMOrchestrator"
    )

    # When Operation is Create, use Invoke-ResourceAssignment
    if ($Operation -eq "Create") {
        # Create a simple config object for justification
        $Config = [PSCustomObject]@{
            # Use the standardized prefix format for justification to enable delta mode detection
            Justification = "Invoke-EasyPIMOrchestrator: Created by EasyPIM Orchestrator on $(Get-Date -Format 'yyyy-MM-dd')"
            ProtectedUsers = $ProtectedUsers
        }

        return Invoke-ResourceAssignment -ResourceType $ResourceType -Assignments $Assignments -CommandMap $CommandMap -Config $Config
    }
    else {
        # For Remove operations, use existing functionality or Invoke-DeltaCleanup
        Write-Warning "Remove operation not implemented in this function. Please use Invoke-DeltaCleanup instead."
        return @{
            Created = 0
            Skipped = 0
            Failed = 0
        }
    }
}

# Create aliases for backward compatibility
Set-Alias -Name Process-PIMAssignments -Value Invoke-PIMAssignment
Set-Alias -Name Process-PIMAssignment -Value Invoke-PIMAssignment

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 Show-EasyPIMUsage {
    [CmdletBinding()]
    param()
    
    Write-Host @"
Usage: Invoke-EasyPIMOrchestrator [Parameters]
 
Required Parameters:
    -TenantId <string>
    -SubscriptionId <string>
     
One of these is required:
    -ConfigFilePath <string>
    -KeyVaultName <string> -SecretName <string>
 
Optional Parameters:
    -Mode <string> Options: "initial" or "delta" (default: "delta")
    -Operations <string[]> Options: "All", "AzureRoles", "EntraRoles", "GroupRoles" (default: "All")
                            You can specify multiple operations, e.g.: -Operations AzureRoles,EntraRoles
    -SkipAssignments Switch to run only cleanup without creating new assignments
 
Examples:
    # Run all operations using a config file
    Invoke-EasyPIMOrchestrator -TenantId "tenant-id" -SubscriptionId "sub-id" -ConfigFilePath "config.json"
 
    # Run only cleanup operations (no new assignments)
    Invoke-EasyPIMOrchestrator -TenantId "tenant-id" -SubscriptionId "sub-id" -ConfigFilePath "config.json" -SkipAssignments
 
    # Run only Azure Role cleanup without new assignments
    Invoke-EasyPIMOrchestrator -TenantId "tenant-id" -SubscriptionId "sub-id" -ConfigFilePath "config.json" -Operations AzureRoles -SkipAssignments
"@
 -ForegroundColor Cyan
}

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
    }
}

# 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
    }
}


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

    # Handle assignment results - might be null if assignments were skipped
    if ($null -ne $AssignmentResults) {
        Write-Host "│ ✅ Created : $($AssignmentResults.Created)" -ForegroundColor White
        Write-Host "│ ⏭️ Skipped : $($AssignmentResults.Skipped)" -ForegroundColor White
        Write-Host "│ ❌ Failed : $($AssignmentResults.Failed)" -ForegroundColor White
    } else {
        Write-Host "│ ✅ Created : 0" -ForegroundColor White
        Write-Host "│ ⏭️ Skipped : 0" -ForegroundColor White
        Write-Host "│ ❌ Failed : 0" -ForegroundColor White
    }
    Write-Host "└───────────────────────────────────────────────────────────────────────────────┘" -ForegroundColor White

    # Cleanup section
    Write-Host "┌───────────────────────────────────────────────────────────────────────────────┐" -ForegroundColor White
    Write-Host "│ CLEANUP OPERATIONS" -ForegroundColor White
    Write-Host "├───────────────────────────────────────────────────────────────────────────────┤" -ForegroundColor White

    # Handle cleanup results - might be null if cleanup was skipped
    if ($null -ne $CleanupResults) {
        # Support for both property naming conventions
        # First try with "Count" suffix, then without
        $kept = if ($null -ne $CleanupResults.KeptCount) { $CleanupResults.KeptCount }
                elseif ($null -ne $CleanupResults.Kept) { $CleanupResults.Kept }
                else { 0 }

        $removed = if ($null -ne $CleanupResults.RemovedCount) { $CleanupResults.RemovedCount }
                elseif ($null -ne $CleanupResults.Removed) { $CleanupResults.Removed }
                else { 0 }

        $skipped = if ($null -ne $CleanupResults.SkippedCount) { $CleanupResults.SkippedCount }
                elseif ($null -ne $CleanupResults.Skipped) { $CleanupResults.Skipped }
                else { 0 }

        $protected = if ($null -ne $CleanupResults.ProtectedCount) { $CleanupResults.ProtectedCount }
                elseif ($null -ne $CleanupResults.Protected) { $CleanupResults.Protected }
                else { 0 }

        Write-Host "│ ✅ Kept : $kept" -ForegroundColor White
        Write-Host "│ 🗑️ Removed : $removed" -ForegroundColor White
        Write-Host "│ ⏭️ Skipped : $skipped" -ForegroundColor White
        if ($protected -gt 0) {
            Write-Host "│ 🛡️ Protected: $protected" -ForegroundColor White
        }
    } else {
        Write-Host "│ ✅ Kept : 0" -ForegroundColor White
        Write-Host "│ 🗑️ Removed : 0" -ForegroundColor White
        Write-Host "│ ⏭️ Skipped : 0" -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"
        
        # Ensure the URI is absolute (starts with https://)
        if (-not $restURI.StartsWith("https://")) {
            # If it's not absolute, prepare to make it absolute
            $baseUrl = "https://management.azure.com"
            
            # If the URI starts with a slash, don't add another one
            if ($restURI.StartsWith("/")) {
                $restURI = "$baseUrl$restURI"
            } else {
                $restURI = "$baseUrl/$restURI"
            }
            Write-Verbose "Converted to absolute URI: $restURI"
        }

        #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,

        [Parameter(Mandatory = $false)]
        [ValidateSet("All", "AzureRoles", "EntraRoles", "GroupRoles")]
        [string[]]$Operations = @("All"),

        [Parameter(Mandatory = $false)]
        [switch]$SkipAssignments,

        [Parameter(Mandatory = $false)]
        [switch]$SkipCleanup
    )

    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 based on selected operations
        $processedConfig = Initialize-EasyPIMAssignments -Config $config

        # Filter config based on selected operations
        if ($Operations -notcontains "All") {
            $filteredConfig = @{}
            foreach ($op in $Operations) {
                switch ($op) {
                    "AzureRoles" {
                        $filteredConfig.AzureRoles = $processedConfig.AzureRoles
                        $filteredConfig.AzureRolesActive = $processedConfig.AzureRolesActive
                    }
                    "EntraRoles" {
                        $filteredConfig.EntraIDRoles = $processedConfig.EntraIDRoles
                        $filteredConfig.EntraIDRolesActive = $processedConfig.EntraIDRolesActive
                    }
                    "GroupRoles" {
                        $filteredConfig.GroupRoles = $processedConfig.GroupRoles
                        $filteredConfig.GroupRolesActive = $processedConfig.GroupRolesActive
                    }
                }
            }
            $processedConfig = $filteredConfig
        }

        # 3. Perform cleanup operations if running full operations or specific role types (skip if requested)
        $cleanupResults = if ($Operations -contains "All" -and -not $SkipCleanup) {
            Invoke-EasyPIMCleanup -Config $processedConfig -Mode $Mode -TenantId $TenantId -SubscriptionId $SubscriptionId
        } else {
            if ($SkipCleanup) {
                Write-Host "⚠️ Skipping cleanup as requested by SkipCleanup parameter" -ForegroundColor Yellow
            } else {
                Write-Host "⚠️ Skipping cleanup as specific operations were selected" -ForegroundColor Yellow
            }
            $null
        }

        # 4. Process assignments (skip if requested)
        if (-not $SkipAssignments) {
            $assignmentResults = New-EasyPIMAssignments -Config $processedConfig -TenantId $TenantId -SubscriptionId $SubscriptionId
        } else {
            Write-Host "⚠️ Skipping assignment creation as requested" -ForegroundColor Yellow
            $assignmentResults = $null
        }

        # 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 $_}