PimRoleTools.psm1
#Requires -Version 7.1 #Requires -Modules Microsoft.Graph.Authentication, Microsoft.Graph.Identity.Governance, Microsoft.Graph.Identity.SignIns <# .SYNOPSIS PimRoleTools - Enhanced PowerShell module for Azure AD and Azure Resource PIM management .DESCRIPTION This module provides comprehensive tools for managing Privileged Identity Management (PIM) roles in both Azure AD (Entra ID) and Azure Resources. It simplifies role activation, monitoring, and management tasks. .AUTHOR Mike Guimaraes .VERSION 2.0.0 #> # Module-level variables $script:ModuleName = 'PimRoleTools' $script:RequiredScopes = @( 'RoleManagement.ReadWrite.Directory', 'PrivilegedAccess.ReadWrite.AzureAD', 'PrivilegedAccess.ReadWrite.AzureResources', 'PrivilegedAccess.ReadWrite.AzureADGroup', 'Directory.Read.All', 'User.Read' ) #region Helper Functions function Test-GraphConnection { <# .SYNOPSIS Verifies Microsoft Graph connection and required permissions #> [CmdletBinding()] param() $context = Get-MgContext if (-not $context) { Write-Verbose "No active Microsoft Graph connection found" return $false } # Check if we have the required scopes $missingScopes = $script:RequiredScopes | Where-Object { $_ -notin $context.Scopes } if ($missingScopes) { Write-Warning "Missing required scopes: $($missingScopes -join ', ')" return $false } return $true } function Connect-PimGraph { <# .SYNOPSIS Establishes connection to Microsoft Graph with required PIM scopes #> [CmdletBinding()] param( [switch]$ForceReconnect ) if (-not $ForceReconnect -and (Test-GraphConnection)) { Write-Verbose "Already connected to Microsoft Graph with required permissions" return } try { Connect-MgGraph -Scopes $script:RequiredScopes -NoWelcome Write-Host "✅ Connected to Microsoft Graph successfully" -ForegroundColor Green } catch { throw "Failed to connect to Microsoft Graph: $_" } } function Format-Duration { <# .SYNOPSIS Formats ISO 8601 duration to human-readable format #> param( [string]$Duration ) if ([string]::IsNullOrEmpty($Duration)) { return "N/A" } try { $timespan = [System.Xml.XmlConvert]::ToTimeSpan($Duration) $parts = @() if ($timespan.Days -gt 0) { $parts += "$($timespan.Days) day$(if($timespan.Days -ne 1){'s'})" } if ($timespan.Hours -gt 0) { $parts += "$($timespan.Hours) hour$(if($timespan.Hours -ne 1){'s'})" } if ($timespan.Minutes -gt 0) { $parts += "$($timespan.Minutes) minute$(if($timespan.Minutes -ne 1){'s'})" } return $parts -join ', ' } catch { return $Duration } } function Format-TimeRemaining { <# .SYNOPSIS Formats remaining time in a human-readable format #> param( [nullable[TimeSpan]]$TimeSpan ) if (-not $TimeSpan -or $TimeSpan.TotalSeconds -le 0) { return "Expired" } $parts = @() if ($TimeSpan.Days -gt 0) { $parts += "$($TimeSpan.Days)d" } if ($TimeSpan.Hours -gt 0) { $parts += "$($TimeSpan.Hours)h" } if ($TimeSpan.Minutes -gt 0) { $parts += "$($TimeSpan.Minutes)m" } if ($parts.Count -eq 0 -and $TimeSpan.Seconds -gt 0) { $parts += "$($TimeSpan.Seconds)s" } return $parts -join ' ' } function Show-ActivationSpinner { <# .SYNOPSIS Shows a spinner animation while waiting for role activation #> param( [string]$RoleName, [scriptblock]$CheckActivation, [int]$MaxWaitSeconds = 300 ) $spinner = @('⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏') $i = 0 $startTime = Get-Date Write-Host -NoNewline "Waiting for activation of '$RoleName' " try { do { $elapsed = (Get-Date) - $startTime if ($elapsed.TotalSeconds -gt $MaxWaitSeconds) { Write-Host ("`r" + " " * 80) # Clear the line Write-Host "`r❌ Activation timeout after $MaxWaitSeconds seconds" -ForegroundColor Red return $false } Write-Host -NoNewline ("`r" + $spinner[$i % $spinner.Length] + " Waiting... ($([int]$elapsed.TotalSeconds)s)") $i++ Start-Sleep -Milliseconds 500 $isActive = & $CheckActivation } while (-not $isActive) # Clear the spinner line completely and show success message Write-Host ("`r" + " " * 80) # Clear the line Write-Host "`r✅ Role '$RoleName' is now active!" -ForegroundColor Green return $true } catch { # Handle Ctrl+C or other interruptions if ($_.Exception.Message -like "*interrupted*" -or $_.CategoryInfo.Category -eq 'OperationStopped') { Write-Host ("`r" + " " * 80) # Clear the line Write-Host "`r⚠️ Activation monitoring interrupted by user" -ForegroundColor Yellow Write-Host " The role may still be activating in the background." -ForegroundColor Gray Write-Host " Use 'Get-PimRole -Status Active' to check status." -ForegroundColor Gray return $false } else { Write-Host ("`r" + " " * 80) # Clear the line Write-Host "`r❌ Error during activation monitoring: $_" -ForegroundColor Red return $false } } } #endregion #region Azure AD (Entra ID) PIM Functions function Get-PimRole { <# .SYNOPSIS Gets all PIM role assignments for the current user (Azure AD/Entra ID) .DESCRIPTION Lists all Azure AD PIM roles (active, eligible, permanent) for the current user with detailed status information .PARAMETER RoleName Filter by specific role name .PARAMETER Status Filter by status: Active, Eligible, Permanent, or All (default) .PARAMETER IncludeDetails Include additional details like role description and permissions .EXAMPLE Get-PimRole Lists all PIM roles for the current user .EXAMPLE Get-PimRole -Status Active Lists only currently active PIM roles .EXAMPLE Get-PimRole -RoleName "Global Administrator" -IncludeDetails Shows detailed information for the Global Administrator role #> [CmdletBinding()] param( [string]$RoleName, [ValidateSet('Active', 'Eligible', 'Permanent', 'All')] [string]$Status = 'All', [switch]$IncludeDetails ) Connect-PimGraph try { $currentUser = Get-MgUser -UserId (Get-MgContext).Account $userId = $currentUser.Id $results = @() # Get eligible roles if ($Status -in 'All', 'Eligible') { $eligibleSchedules = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -All ` -Filter "principalId eq '$userId'" -ExpandProperty RoleDefinition foreach ($schedule in $eligibleSchedules) { $results += [PSCustomObject]@{ RoleName = $schedule.RoleDefinition.DisplayName RoleId = $schedule.RoleDefinition.Id Status = 'Eligible' StartTime = $null EndTime = $null TimeRemaining = $null DirectoryScopeId = $schedule.DirectoryScopeId MemberType = $schedule.MemberType } } } # Get active roles if ($Status -in 'All', 'Active') { $activeSchedules = Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance -All ` -Filter "principalId eq '$userId'" -ExpandProperty RoleDefinition,ActivatedUsing foreach ($schedule in $activeSchedules) { $endTime = if ($schedule.EndDateTime) { [DateTime]::Parse($schedule.EndDateTime).ToLocalTime() } else { $null } $timeRemaining = if ($endTime) { $endTime - (Get-Date) } else { $null } $results += [PSCustomObject]@{ RoleName = $schedule.RoleDefinition.DisplayName RoleId = $schedule.RoleDefinition.Id Status = 'Active' StartTime = if ($schedule.StartDateTime) { [DateTime]::Parse($schedule.StartDateTime).ToLocalTime() } else { $null } EndTime = $endTime TimeRemaining = $timeRemaining DirectoryScopeId = $schedule.DirectoryScopeId MemberType = $schedule.MemberType } } } # Get permanent assignments if ($Status -in 'All', 'Permanent') { $permanentAssignments = Get-MgRoleManagementDirectoryRoleAssignment -All ` -Filter "principalId eq '$userId'" -ExpandProperty RoleDefinition foreach ($assignment in $permanentAssignments) { # Check if this is not already in active (to avoid duplicates) $isDuplicate = $results | Where-Object { $_.RoleId -eq $assignment.RoleDefinition.Id -and $_.Status -eq 'Active' } if (-not $isDuplicate) { $results += [PSCustomObject]@{ RoleName = $assignment.RoleDefinition.DisplayName RoleId = $assignment.RoleDefinition.Id Status = 'Permanent' StartTime = $null EndTime = $null TimeRemaining = $null DirectoryScopeId = $assignment.DirectoryScopeId MemberType = 'Direct' } } } } # Filter by role name if specified if ($RoleName) { $results = $results | Where-Object { $_.RoleName -like "*$RoleName*" } } # Add details if requested if ($IncludeDetails -and $results) { foreach ($result in $results) { $roleDefinition = Get-MgRoleManagementDirectoryRoleDefinition -UnifiedRoleDefinitionId $result.RoleId $result | Add-Member -NotePropertyName Description -NotePropertyValue $roleDefinition.Description $result | Add-Member -NotePropertyName IsBuiltIn -NotePropertyValue $roleDefinition.IsBuiltIn } } # Sort results $results | Sort-Object Status, RoleName } catch { Write-Error "Failed to retrieve PIM roles: $_" } } function Enable-PimRole { <# .SYNOPSIS Activates an eligible PIM role for the current user (Azure AD/Entra ID) .DESCRIPTION Activates an eligible Azure AD PIM role with optional duration, justification, and ticket information .PARAMETER RoleName The name of the role to activate (supports wildcards) .PARAMETER Duration ISO 8601 duration (default: PT8H). Examples: PT4H (4 hours), PT30M (30 minutes) .PARAMETER Justification Reason for activation .PARAMETER TicketNumber Optional ticket number for auditing .PARAMETER TicketSystem Optional ticket system name .PARAMETER NoWait Don't wait for activation to complete .EXAMPLE Enable-PimRole -RoleName "Global Administrator" Activates the Global Administrator role for 8 hours .EXAMPLE Enable-PimRole -RoleName "User Admin*" -Duration PT4H -Justification "User management tasks" Activates a role matching "User Admin*" for 4 hours #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string]$RoleName, [string]$Duration = "PT8H", [string]$Justification, [string]$TicketNumber, [string]$TicketSystem, [switch]$NoWait ) Connect-PimGraph # Find eligible role $eligibleRoles = Get-PimRole -Status Eligible -RoleName $RoleName if (-not $eligibleRoles) { Write-Error "No eligible roles found matching '$RoleName'" return } if ($eligibleRoles.Count -gt 1) { Write-Host "Multiple eligible roles found matching '$RoleName':" -ForegroundColor Yellow $eligibleRoles | Format-Table RoleName, DirectoryScopeId -AutoSize Write-Error "Please specify a more specific role name" return } $role = $eligibleRoles[0] # Set default justification if not provided if (-not $Justification) { $Justification = "Activating $($role.RoleName) for administrative tasks" } if ($PSCmdlet.ShouldProcess($role.RoleName, "Activate PIM Role")) { try { $params = @{ Action = "selfActivate" PrincipalId = (Get-MgUser -UserId (Get-MgContext).Account).Id RoleDefinitionId = $role.RoleId DirectoryScopeId = $role.DirectoryScopeId Justification = $Justification ScheduleInfo = @{ StartDateTime = (Get-Date).ToUniversalTime() Expiration = @{ Type = "AfterDuration" Duration = $Duration } } } # Add ticket info if provided if ($TicketNumber -or $TicketSystem) { $params.TicketInfo = @{} if ($TicketNumber) { $params.TicketInfo.TicketNumber = $TicketNumber } if ($TicketSystem) { $params.TicketInfo.TicketSystem = $TicketSystem } } # Submit activation request $request = New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -BodyParameter $params Write-Host "✅ PIM activation request submitted for '$($role.RoleName)'" -ForegroundColor Green Write-Host " Duration: $(Format-Duration $Duration)" -ForegroundColor Gray Write-Host " Justification: $Justification" -ForegroundColor Gray # Wait for activation unless -NoWait is specified if (-not $NoWait) { $checkScript = { try { $activeRoles = Get-PimRole -Status Active -RoleName $role.RoleName $result = ($activeRoles | Where-Object { $_.RoleId -eq $role.RoleId }) return [bool]$result } catch { # Return false on error to keep trying return $false } } try { $spinnerResult = Show-ActivationSpinner -RoleName $role.RoleName -CheckActivation $checkScript # Clear any remaining output if (-not $spinnerResult) { Write-Host "" # Add newline if spinner was interrupted } } catch { # Handle any interruptions during spinner Write-Host "`n⚠️ Activation monitoring stopped" -ForegroundColor Yellow Write-Host " Check activation status with: Get-PimRole -Status Active -RoleName '$($role.RoleName)'" -ForegroundColor Gray } } } catch { Write-Error "Failed to activate role: $_" } } } function Disable-PimRole { <# .SYNOPSIS Deactivates an active PIM role (Azure AD/Entra ID) .DESCRIPTION Deactivates a currently active Azure AD PIM role assignment Note: Azure AD requires roles to be active for at least 5 minutes before deactivation .PARAMETER RoleName The name of the role to deactivate .PARAMETER Force Skip confirmation prompt .EXAMPLE Disable-PimRole -RoleName "Global Administrator" Deactivates the Global Administrator role #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string]$RoleName, [switch]$Force ) Connect-PimGraph # Find active role $activeRoles = Get-PimRole -Status Active -RoleName $RoleName if (-not $activeRoles) { Write-Error "No active roles found matching '$RoleName'" return } if ($activeRoles.Count -gt 1) { Write-Host "Multiple active roles found matching '$RoleName':" -ForegroundColor Yellow $activeRoles | Format-Table RoleName, DirectoryScopeId, TimeRemaining -AutoSize Write-Error "Please specify a more specific role name" return } $role = $activeRoles[0] # Check if role has been active for at least 5 minutes if ($role.StartTime) { $activeDuration = (Get-Date) - $role.StartTime if ($activeDuration.TotalMinutes -lt 5) { $remainingWait = [TimeSpan]::FromMinutes(5) - $activeDuration Write-Host "⏳ Role must be active for at least 5 minutes before deactivation" -ForegroundColor Yellow Write-Host " Time remaining: $(Format-TimeRemaining $remainingWait)" -ForegroundColor Gray Write-Host " The role will auto-expire at: $($role.EndTime)" -ForegroundColor Gray return } } if ($Force -or $PSCmdlet.ShouldProcess($role.RoleName, "Deactivate PIM Role")) { try { $params = @{ Action = "selfDeactivate" PrincipalId = (Get-MgUser -UserId (Get-MgContext).Account).Id RoleDefinitionId = $role.RoleId DirectoryScopeId = $role.DirectoryScopeId Justification = "Deactivating role - task completed" } New-MgRoleManagementDirectoryRoleAssignmentScheduleRequest -BodyParameter $params Write-Host "✅ Successfully deactivated '$($role.RoleName)'" -ForegroundColor Green } catch { # Handle specific error cases if ($_.Exception.Message -like "*ActiveDurationTooShort*" -or $_.Exception.Message -like "*Miniumum Required is 5 minutes*") { Write-Host "⏳ Cannot deactivate: Role must be active for at least 5 minutes" -ForegroundColor Yellow Write-Host " This is an Azure AD PIM requirement for security" -ForegroundColor Gray if ($role.EndTime) { Write-Host " Role will auto-expire at: $($role.EndTime)" -ForegroundColor Gray } } else { Write-Error "Failed to deactivate role: $_" } } } } function Show-PimRole { <# .SYNOPSIS Shows a detailed summary of a specific PIM role (Azure AD/Entra ID) .DESCRIPTION Displays comprehensive information about a PIM role assignment including status, timing, and permissions .PARAMETER RoleName The name of the role to display .EXAMPLE Show-PimRole -RoleName "Global Administrator" Shows detailed information about the Global Administrator role #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$RoleName ) $roles = Get-PimRole -RoleName $RoleName -IncludeDetails if (-not $roles) { Write-Host "ℹ️ No PIM roles found matching '$RoleName'" -ForegroundColor Yellow return } foreach ($role in $roles) { Write-Host "`n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan Write-Host "🛡️ Role: $($role.RoleName)" -ForegroundColor White Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan # Status with color coding $statusColor = switch ($role.Status) { 'Active' { 'Green' } 'Eligible' { 'Yellow' } 'Permanent' { 'Cyan' } default { 'White' } } Write-Host "Status : " -NoNewline Write-Host $role.Status -ForegroundColor $statusColor if ($role.Status -eq 'Active') { Write-Host "Start Time : $($role.StartTime)" -ForegroundColor Gray Write-Host "End Time : $($role.EndTime)" -ForegroundColor Gray Write-Host "Time Remaining: " -NoNewline if ($role.TimeRemaining -and $role.TimeRemaining.TotalMinutes -gt 0) { $remainingFormatted = Format-TimeRemaining $role.TimeRemaining $warningColor = if ($role.TimeRemaining.TotalMinutes -lt 30) { 'Red' } elseif ($role.TimeRemaining.TotalHours -lt 1) { 'Yellow' } else { 'Green' } Write-Host $remainingFormatted -ForegroundColor $warningColor } else { Write-Host "Expired" -ForegroundColor Red } } elseif ($role.Status -eq 'Eligible') { Write-Host "You are eligible to activate this role" -ForegroundColor Yellow Write-Host "Use: Enable-PimRole -RoleName `"$($role.RoleName)`"" -ForegroundColor Gray } elseif ($role.Status -eq 'Permanent') { Write-Host "This is a permanent assignment (always active)" -ForegroundColor Cyan } Write-Host "Directory Scope: $($role.DirectoryScopeId)" -ForegroundColor Gray Write-Host "Member Type : $($role.MemberType)" -ForegroundColor Gray if ($role.Description) { Write-Host "`nDescription:" -ForegroundColor White Write-Host $role.Description -ForegroundColor Gray } } } #endregion #region Group PIM Functions function Get-PimGroupRole { <# .SYNOPSIS Gets PIM assignments for Azure AD groups .DESCRIPTION Lists all PIM group memberships (owner/member) for the current user .PARAMETER GroupName Filter by group name .PARAMETER Status Filter by status: Active, Eligible, or All (default) .PARAMETER AccessLevel Filter by access level: Member, Owner, or All (default) .EXAMPLE Get-PimGroupRole Lists all PIM group assignments #> [CmdletBinding()] param( [string]$GroupName, [ValidateSet('Active', 'Eligible', 'All')] [string]$Status = 'All', [ValidateSet('Member', 'Owner', 'All')] [string]$AccessLevel = 'All' ) Connect-PimGraph try { $currentUser = Get-MgUser -UserId (Get-MgContext).Account $results = @() # Get eligible assignments if ($Status -in 'All', 'Eligible') { $filter = "principalId eq '$($currentUser.Id)'" $eligibleSchedules = Get-MgIdentityGovernancePrivilegedAccessGroupEligibilitySchedule -All -Filter $filter foreach ($schedule in $eligibleSchedules) { if ($AccessLevel -ne 'All' -and $schedule.AccessId -ne $AccessLevel.ToLower()) { continue } # Get group details $group = Get-MgGroup -GroupId $schedule.GroupId -ErrorAction SilentlyContinue if ($GroupName -and $group.DisplayName -notlike "*$GroupName*") { continue } $results += [PSCustomObject]@{ GroupName = $group.DisplayName GroupId = $schedule.GroupId AccessLevel = $schedule.AccessId Status = 'Eligible' StartTime = $null EndTime = $null TimeRemaining = $null } } } # Get active assignments if ($Status -in 'All', 'Active') { $filter = "principalId eq '$($currentUser.Id)'" $activeSchedules = Get-MgIdentityGovernancePrivilegedAccessGroupAssignmentScheduleInstance -All -Filter $filter foreach ($schedule in $activeSchedules) { if ($AccessLevel -ne 'All' -and $schedule.AccessId -ne $AccessLevel.ToLower()) { continue } # Get group details $group = Get-MgGroup -GroupId $schedule.GroupId -ErrorAction SilentlyContinue if ($GroupName -and $group.DisplayName -notlike "*$GroupName*") { continue } $endTime = if ($schedule.EndDateTime) { [DateTime]::Parse($schedule.EndDateTime).ToLocalTime() } else { $null } $timeRemaining = if ($endTime) { $endTime - (Get-Date) } else { $null } $results += [PSCustomObject]@{ GroupName = $group.DisplayName GroupId = $schedule.GroupId AccessLevel = $schedule.AccessId Status = 'Active' StartTime = if ($schedule.StartDateTime) { [DateTime]::Parse($schedule.StartDateTime).ToLocalTime() } else { $null } EndTime = $endTime TimeRemaining = $timeRemaining } } } $results | Sort-Object Status, GroupName, AccessLevel } catch { Write-Error "Failed to retrieve PIM group assignments: $_" } } function Enable-PimGroupRole { <# .SYNOPSIS Activates eligible PIM group membership .DESCRIPTION Activates an eligible PIM group membership (owner or member role) .PARAMETER GroupName The name of the group .PARAMETER AccessLevel The access level to activate: Member or Owner .PARAMETER Duration ISO 8601 duration (default: PT8H) .PARAMETER Justification Reason for activation .EXAMPLE Enable-PimGroupRole -GroupName "IT Admins" -AccessLevel Member Activates member access to the IT Admins group #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string]$GroupName, [Parameter(Mandatory)] [ValidateSet('Member', 'Owner')] [string]$AccessLevel, [string]$Duration = "PT8H", [string]$Justification ) Connect-PimGraph # Find the eligible group assignment $eligibleGroups = Get-PimGroupRole -GroupName $GroupName -Status Eligible -AccessLevel $AccessLevel if (-not $eligibleGroups) { Write-Error "No eligible $AccessLevel assignment found for group '$GroupName'" return } $group = $eligibleGroups[0] if (-not $Justification) { $Justification = "Activating $AccessLevel access to $($group.GroupName)" } if ($PSCmdlet.ShouldProcess("$AccessLevel access to $($group.GroupName)", "Activate PIM Group Assignment")) { try { $params = @{ AccessId = $AccessLevel.ToLower() PrincipalId = (Get-MgUser -UserId (Get-MgContext).Account).Id GroupId = $group.GroupId Action = "selfActivate" ScheduleInfo = @{ StartDateTime = (Get-Date).ToUniversalTime() Expiration = @{ Type = "afterDuration" Duration = $Duration } } Justification = $Justification } $request = New-MgIdentityGovernancePrivilegedAccessGroupAssignmentScheduleRequest -BodyParameter $params Write-Host "✅ PIM group activation request submitted" -ForegroundColor Green Write-Host " Group: $($group.GroupName)" -ForegroundColor Gray Write-Host " Access Level: $AccessLevel" -ForegroundColor Gray Write-Host " Duration: $(Format-Duration $Duration)" -ForegroundColor Gray } catch { Write-Error "Failed to activate group membership: $_" } } } #endregion #region Summary Functions function Get-PimSummary { <# .SYNOPSIS Gets a comprehensive summary of all PIM assignments .DESCRIPTION Shows a consolidated view of all PIM assignments across Azure AD roles, Azure resources, and groups .PARAMETER IncludeInactive Include eligible and permanent assignments in addition to active ones .EXAMPLE Get-PimSummary Shows all active PIM assignments .EXAMPLE Get-PimSummary -IncludeInactive Shows all PIM assignments including eligible ones #> [CmdletBinding()] param( [switch]$IncludeInactive ) Connect-PimGraph Write-Host "`n═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan Write-Host " PIM ASSIGNMENT SUMMARY " -ForegroundColor White Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan # Azure AD Roles Write-Host "`n▶ Azure AD (Entra ID) Roles:" -ForegroundColor Yellow $aadRoles = if ($IncludeInactive) { Get-PimRole } else { Get-PimRole -Status Active } if ($aadRoles) { $aadRoles | Format-Table -Property @( @{Label="Role"; Expression={$_.RoleName}; Width=40} @{Label="Status"; Expression={$_.Status}; Width=12} @{Label="Time Remaining"; Expression={ if ($_.Status -eq 'Active' -and $_.TimeRemaining) { Format-TimeRemaining $_.TimeRemaining } else { '-' } }; Width=15} ) -AutoSize } else { Write-Host " No assignments found" -ForegroundColor Gray } # Group Memberships Write-Host "`n▶ Group Memberships:" -ForegroundColor Yellow $groupRoles = if ($IncludeInactive) { Get-PimGroupRole } else { Get-PimGroupRole -Status Active } if ($groupRoles) { $groupRoles | Format-Table -Property @( @{Label="Group"; Expression={$_.GroupName}; Width=40} @{Label="Access"; Expression={$_.AccessLevel}; Width=10} @{Label="Status"; Expression={$_.Status}; Width=12} @{Label="Time Remaining"; Expression={ if ($_.Status -eq 'Active' -and $_.TimeRemaining) { Format-TimeRemaining $_.TimeRemaining } else { '-' } }; Width=15} ) -AutoSize } else { Write-Host " No assignments found" -ForegroundColor Gray } Write-Host "`n═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan } #endregion # Export module members Export-ModuleMember -Function @( # Connection 'Connect-PimGraph' # Azure AD/Entra ID 'Get-PimRole' 'Enable-PimRole' 'Disable-PimRole' 'Show-PimRole' # Groups 'Get-PimGroupRole' 'Enable-PimGroupRole' # Summary 'Get-PimSummary' ) |