Public/Start-PIMActivation.ps1

function Start-PIMActivation {
    <#
    .SYNOPSIS
        Starts the PIM Role Activation graphical interface with advanced duplicate role handling.
     
    .DESCRIPTION
        Launches a Windows Forms application for managing Privileged Identity Management (PIM) role activations.
        The application provides an intuitive interface for activating Entra ID roles, PIM-enabled groups,
        and Azure Resource roles with sophisticated handling of duplicate role assignments from multiple sources.
         
        Key Features:
        - 85% faster loading through batch API operations
        - Smart duplicate role handling showing correct group attribution
        - Activation profiles for reusable role combinations
        - Azure Resource reduced-scope activation picker
        - Persistent policy metadata cache for faster repeat launches
        - Authentication context batching where roles share the same context requirement
        - Visual indication of role sources (Direct vs Group-derived)
        - Automatic handling of authentication and module dependencies
         
        Requirements:
        - PowerShell 7.0 or later
        - Single-threaded apartment (STA) mode for Windows Forms
        - Microsoft Graph PowerShell modules available at module import time
         
        The tool automatically handles authentication, imports validated modules, and provides a loading
        interface with progress tracking during initialization.
     
    .PARAMETER IncludeEntraRoles
        Include Entra ID (Azure AD) roles in the activation interface.
        When enabled, displays available Entra ID role assignments that can be activated.
        Shows both direct assignments and group-derived roles with proper attribution.
        Default: $true
     
    .PARAMETER IncludeGroups
        Include PIM-enabled security groups in the activation interface.
        When enabled, displays eligible group memberships that can be activated.
        Groups that provide Entra ID roles will show those roles upon activation.
        Default: $true
     
    .PARAMETER IncludeAzureResources
        Include Azure resource roles (RBAC) in the activation interface.
        When enabled, displays eligible and active Azure Resource PIM assignments across accessible
        subscriptions and supports Azure Resource activation, deactivation, policy detection, and reduced scope.
        Default: $false
     
    .PARAMETER Verbose
        Enables verbose output for troubleshooting.
        Shows detailed information about role processing, API calls, and group attribution logic.
     
    .EXAMPLE
        Start-PIMActivation
         
        Launches the PIM activation interface with default settings.
        Includes Entra ID roles and PIM-enabled groups with fast batch loading.
     
    .EXAMPLE
        Start-PIMActivation -Verbose
         
        Launches the interface with detailed logging output.
        Useful for troubleshooting duplicate role attribution or connection issues.
     
    .EXAMPLE
        Start-PIMActivation -IncludeEntraRoles:$false
         
        Launches the interface showing only PIM-enabled groups.
        Excludes Entra ID role assignments from the display.
     
    .NOTES
        Name: Start-PIMActivation
        Author: Sebastian Flæng Markdanner
        Version: 2.2.0
         
        This function requires PowerShell 7+ and will automatically restart in STA mode if needed.
        Required Graph and Azure modules are validated and loaded when the module is imported.
         
        The function maintains session state for account switching and can restart itself
        when users need to switch between different Microsoft accounts.
     
    .LINK
        https://github.com/Noble-Effeciency13/PIMActivation
     
    .LINK
        https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([void])]
    param(
        [Parameter(HelpMessage = "Include Entra ID roles in the activation interface")]
        [switch]$IncludeEntraRoles,
        
        [Parameter(HelpMessage = "Include PIM-enabled groups in the activation interface")]
        [switch]$IncludeGroups,
        
        [Parameter(HelpMessage = "Include Azure resource roles in the activation interface")]
        [switch]$IncludeAzureResources,
        
        [Parameter(HelpMessage = "Force module reinitialization if import-time dependency validation must be retried")]
        [switch]$Force,
        
        [Parameter(HelpMessage = "Deprecated compatibility switch; dependency validation happens at module import")]
        [switch]$ManualDependencyCheck,

        [Parameter(HelpMessage = "Client ID of the app registration to use for Graph auth")]
        [string]$ClientId,

        [Parameter(HelpMessage = "Tenant ID to use with the specified app registration")]
        [string]$TenantId,
        
        [Parameter(HelpMessage = "Enable parallel processing for Azure subscriptions and PIM policies (requires PowerShell 7+)")]
        [switch]$DisableParallelProcessing,
        
        [Parameter(HelpMessage = "Maximum concurrent operations for parallel processing of subscriptions and policies")]
        [int]$ThrottleLimit = 10
    )
    
begin {
    # Set default values for switches (PowerShell best practice)
    if (-not $PSBoundParameters.ContainsKey('IncludeEntraRoles')) { $IncludeEntraRoles = $true }
    if (-not $PSBoundParameters.ContainsKey('IncludeGroups')) { $IncludeGroups = $true }
    Write-Verbose "Starting PIM Activation Tool initialization"

    # Initialize script-scoped caches safely (avoid unset variable runtime errors)
    if (-not (Get-Variable -Name 'RoleCacheValidityMinutes' -Scope Script -ErrorAction SilentlyContinue)) {
        $script:RoleCacheValidityMinutes = 10
    }
    if (-not (Get-Variable -Name 'CachedEligibleRoles' -Scope Script -ErrorAction SilentlyContinue)) {
        $script:CachedEligibleRoles = @()
    }
    if (-not (Get-Variable -Name 'CachedActiveRoles' -Scope Script -ErrorAction SilentlyContinue)) {
        $script:CachedActiveRoles = @()
    }
    if (-not (Get-Variable -Name 'LastRoleFetchTime' -Scope Script -ErrorAction SilentlyContinue)) {
        $script:LastRoleFetchTime = $null
    }
    if (-not (Get-Variable -Name 'PolicyCache' -Scope Script -ErrorAction SilentlyContinue)) {
        $script:PolicyCache = @{}
    }
    if (-not (Get-Variable -Name 'AuthenticationContextCache' -Scope Script -ErrorAction SilentlyContinue)) {
        $script:AuthenticationContextCache = @{}
    }
    if (-not (Get-Variable -Name 'AzureRolesCache' -Scope Script -ErrorAction SilentlyContinue)) {
        $script:AzureRolesCache = @()
    }
    if (-not (Get-Variable -Name 'AzureRolesCacheTime' -Scope Script -ErrorAction SilentlyContinue)) {
        $script:AzureRolesCacheTime = $null
    }

    # Validate PowerShell version requirement
    if ($PSVersionTable.PSVersion.Major -lt 7) {
        $errorMessage = "PowerShell 7 or later is required. Current version: $($PSVersionTable.PSVersion). Please upgrade from https://aka.ms/powershell"
        Write-Error $errorMessage -Category InvalidOperation
        throw $errorMessage
    }

    if ($ManualDependencyCheck) {
        Write-Verbose "ManualDependencyCheck is deprecated; dependency validation now happens at module import."
    }

    if ($script:DependenciesValidated) {
        Write-Verbose "Module dependencies already validated at import time."
    }
    else {
        Write-Verbose "Import-time dependency validation did not complete; Start-PIMActivation will retry module imports during startup."
    }

    if (-not $PSCmdlet.ShouldProcess("PIM Activation Tool", "Start PIM role activation interface")) {
        Write-Verbose "Operation cancelled by user"
        return
    }

    # Configure execution preferences
    $originalVerbosePreference = $VerbosePreference
    $originalWarningPreference = $WarningPreference
    $originalProgressPreference = $ProgressPreference

    # Preserve user's verbose preference while silencing other noise
    if ($PSCmdlet.MyInvocation.BoundParameters.ContainsKey('Verbose')) {
        $script:UserVerbose = $true
        Write-Verbose "Verbose output enabled by user"
    }
    else {
        $script:UserVerbose = $false
    }

    $WarningPreference = 'SilentlyContinue'
    $ProgressPreference = 'SilentlyContinue'

    # Suppress Azure PowerShell breaking change warnings
    $env:SuppressAzurePowerShellBreakingChangeWarnings = 'true'

    # Initialize session state variables
    $script:RestartRequested = $false

    Write-Verbose "Initialization parameters: EntraRoles=$IncludeEntraRoles, Groups=$IncludeGroups, AzureResources=$IncludeAzureResources"
}
    
    process {
        # Set up verbose preference early (needed for Initialize-PIMModules call)
        if ($PSCmdlet.MyInvocation.BoundParameters.ContainsKey('Verbose')) {
            $script:UserVerbose = $true
        }
        else {
            $script:UserVerbose = $false
        }
        
        try {
            # Ensure Single-Threaded Apartment mode for Windows Forms
            if (-not (Test-STAMode)) {
                Write-Verbose "Restarting in STA mode for Windows Forms compatibility"
                return Start-STAProcess -Parameters $PSBoundParameters
            }
            
            # Store parameters for potential restart scenarios (account switching)
            $script:StartupParameters = $PSBoundParameters
            $script:IncludeEntraRoles = $IncludeEntraRoles
            $script:IncludeGroups = $IncludeGroups
            $script:IncludeAzureResources = $IncludeAzureResources
            
            # Load required .NET assemblies for Windows Forms
            Write-Verbose "Loading Windows Forms assemblies"
            Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop
            Add-Type -AssemblyName System.Drawing -ErrorAction Stop
            
            # Initialize loading interface
            $splashForm = Show-LoadingSplash -Message "Initializing PIM Activation Tool..."
            
            # Give splash form time to render
            Start-Sleep -Milliseconds 200
            
            try {
                # Dependencies are normally preloaded when PIMActivation is imported. Retry here only
                # if import-time validation failed.
                if (-not $script:DependenciesValidated) {
                    $moduleStatus = "Loading dependencies..."
                    Write-Verbose $moduleStatus
                    Update-LoadingStatus -SplashForm $splashForm -Status $moduleStatus -Progress 30

                    $moduleParams = @{}
                    if ($Force) { $moduleParams.Force = $true }
                    $moduleParams.IncludeAzureModules = $true

                    $moduleResult = Initialize-PIMModules @moduleParams
                    if (-not $moduleResult.Success) {
                        throw "Required module initialization failed: $($moduleResult.Error)"
                    }

                    foreach ($mod in @(
                        'Microsoft.Graph.Authentication',
                        'Microsoft.Graph.Identity.DirectoryManagement',
                        'Microsoft.Graph.Identity.Governance',
                        'Microsoft.Graph.Identity.SignIns',
                        'Microsoft.Graph.Groups',
                        'Microsoft.Graph.Users',
                        'Az.Accounts',
                        'Az.Resources'
                    )) {
                        if (-not (Import-PIMModule -ModuleName $mod -Force:$Force)) {
                            throw "Failed to load required module: $mod"
                        }
                    }
                    $script:DependenciesValidated = $true
                }
                else {
                    Write-Verbose "Dependencies pre-loaded at import time; startup module loading skipped"
                }

                Update-LoadingStatus -SplashForm $splashForm -Status "Modules ready..." -Progress 30
                
                # Establish service connections
                Write-Verbose "Connecting to Microsoft services"
                Update-LoadingStatus -SplashForm $splashForm -Status "Connecting to Microsoft Graph..." -Progress 50
                
                # Build params only if explicitly provided
                $connectionParams = @{
                    IncludeEntraRoles     = $script:IncludeEntraRoles
                    IncludeGroups         = $script:IncludeGroups
                    IncludeAzureResources = $script:IncludeAzureResources
                }
                if ($PSBoundParameters.ContainsKey('ClientId') -and $ClientId) {
                    $connectionParams.ClientId = $ClientId
                }
                if ($PSBoundParameters.ContainsKey('TenantId') -and $TenantId) {
                    $connectionParams.TenantId = $TenantId
                }
                
                # Delegate fine-grained status updates to Connect-PIMServices
                $connectionParams.SplashForm = $splashForm
                $connectionResult = Connect-PIMServices @connectionParams
                
                if (-not $connectionResult.Success) {
                    throw "Authentication failed: $($connectionResult.Error)"
                }
                
                Write-Verbose "Connected as user: $($connectionResult.CurrentUser.UserPrincipalName)"
                # Progress updates from Connect-PIMServices already advanced
                Update-LoadingStatus -SplashForm $splashForm -Status "Loading user profile..." -Progress 70
                
                # Store connection context for session management
                $script:CurrentUser = $connectionResult.CurrentUser
                $script:GraphContext = $connectionResult.GraphContext

                try {
                    Update-LoadingStatus -SplashForm $splashForm -Status "Loading cached policy metadata..." -Progress 74
                    $policyCacheImport = Import-PIMPolicyCache
                    if ($policyCacheImport -and $policyCacheImport.PolicyCount -gt 0) {
                        Write-Verbose "Loaded cached policy metadata: $($policyCacheImport.PolicyCount) policies"
                    }
                }
                catch {
                    Write-Verbose "Persistent policy cache load skipped: $($_.Exception.Message)"
                }
                
                # Initialize main application form
                Write-Verbose "Building main application interface"
                Update-LoadingStatus -SplashForm $splashForm -Status "Building interface..." -Progress 80
                
                $form = Initialize-PIMForm -SplashForm $splashForm -DisableParallelProcessing:$DisableParallelProcessing -ThrottleLimit $ThrottleLimit -Verbose:$script:UserVerbose
                
                if (-not $form) {
                    throw "Failed to create main application form"
                }
                
                # Launch main application
                Write-Verbose "Launching PIM Activation interface"
                [System.Windows.Forms.Application]::EnableVisualStyles()
                [void]$form.ShowDialog()
                
                # Handle restart requests (typically for account switching)
                if ($script:RestartRequested) {
                    Write-Verbose "Processing restart request for account switch"
                    $script:RestartRequested = $false
                    Start-Sleep -Milliseconds 500  # Allow clean shutdown
                    
                    # Restart with same parameters
                    Start-PIMActivation @script:StartupParameters
                }
            }
            finally {
                # Clean up loading interface
                if ($splashForm -and -not $splashForm.IsDisposed) {
                    Close-LoadingSplash -SplashForm $splashForm
                }
            }
        }
        catch {
            $errorMessage = "PIM Activation Tool failed to start: $($_.Exception.Message)"
            Write-Error $errorMessage -Category OperationStopped
            Write-Verbose "Error details: $($_.ScriptStackTrace)"
            
            # Display user-friendly error dialog
            try {
                [System.Windows.Forms.MessageBox]::Show(
                    "Failed to start PIM Activation Tool:{0}{0}$($_.Exception.Message)" -f [Environment]::NewLine,
                    "PIM Activation Error",
                    [System.Windows.Forms.MessageBoxButtons]::OK,
                    [System.Windows.Forms.MessageBoxIcon]::Error
                )
            }
            catch {
                # Fallback if MessageBox fails
                Write-Host "Error: $errorMessage" -ForegroundColor Red
            }
            
            throw
        }
        finally {
            # Session cleanup
            if ($script:CurrentUser) {
                Write-Verbose "Cleaning up session for: $($script:CurrentUser.UserPrincipalName)"
            }
            
            # Avoid disconnection during restart to maintain session state
            if (-not $script:RestartRequested) {
                try {
                    Write-Verbose "Disconnecting from services"
                    Disconnect-PIMServices -ErrorAction SilentlyContinue
                }
                catch {
                    Write-Verbose "Non-critical error during service disconnection: $($_.Exception.Message)"
                }
            }
            
            # Restore original preferences
            $VerbosePreference = $originalVerbosePreference
            $WarningPreference = $originalWarningPreference
            $ProgressPreference = $originalProgressPreference
        }
    }
    
    end {
        Write-Verbose "PIM Activation Tool session completed"
    }
}