Public/Get-TntPIMReport.ps1

function Get-TntPIMReport {
    <#
    .SYNOPSIS
        Generates a PIM (Privileged Identity Management) report for Azure AD roles.

    .DESCRIPTION
        This function analyzes PIM eligible and active role assignments, providing insights into
        PIM adoption, coverage, and activation patterns. This function REQUIRES an Azure AD Premium P2
        license to access PIM data.

        For permanent role assignments and emergency accounts (no P2 required), use Get-TntPrivilegedRoleReport.

    .PARAMETER TenantId
        The Azure AD Tenant ID (GUID) to connect to.

    .PARAMETER ClientId
        The Application (Client) ID of the app registration created for security reporting.

    .PARAMETER ClientSecret
        The client secret for the app registration. Use this for automated scenarios.

    .PARAMETER CertificateThumbprint
        The thumbprint of the certificate to use for authentication instead of client secret.

    .EXAMPLE
        Get-TntPIMReport -TenantId $tenantId -ClientId $clientId -ClientSecret $secret

        Generates a comprehensive PIM report for eligible and active role assignments.

    .INPUTS
        None. This function does not accept pipeline input.

    .OUTPUTS
        System.Management.Automation.PSCustomObject
        Returns a PIM report object with:
        - Summary: Statistics on PIM usage
        - PIMEligibleAssignments: Detailed list of eligible role assignments
        - PIMActiveAssignments: Detailed list of currently active PIM assignments
        - AssignmentsByRole: Grouped statistics per role

    .NOTES
        Author: Tom de Leeuw
        Website: https://systom.dev
        Module: TenantReports

        Required Permissions:
        - RoleManagement.Read.Directory (Application)
        - Directory.Read.All (Application)

        Prerequisites:
        - **Azure AD Premium P2 license REQUIRED** for PIM capabilities
        - Security Reader, Global Reader, or equivalent role to query PIM assignments

    .LINK
        https://github.com/systommy/TenantReports
    #>


    [CmdletBinding(DefaultParameterSetName = 'ClientSecret')]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ClientSecret')]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Certificate')]
        [Parameter(ParameterSetName = 'Interactive')]
        [ValidateNotNullOrEmpty()]
        [Alias('Tenant')]
        [string]$TenantId,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ClientSecret')]
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Certificate')]
        [Parameter(ParameterSetName = 'Interactive')]
        [ValidatePattern('^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$')]
        [Alias('ApplicationId')]
        [string]$ClientId,

        [Parameter(Mandatory = $true, ParameterSetName = 'ClientSecret', ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('Secret', 'ApplicationSecret')]
        [SecureString]$ClientSecret,

        [Parameter(Mandatory = $true, ParameterSetName = 'Certificate', ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('Thumbprint')]
        [string]$CertificateThumbprint,

        # Use interactive authentication (no app registration required).
        [Parameter(Mandatory = $true, ParameterSetName = 'Interactive')]
        [switch]$Interactive
    )

    begin {
        Write-Information 'Starting PIM report generation...' -InformationAction Continue
    }

    process {
        try {
            # Establish connection
            $ConnectionParams = Get-ConnectionParameters -BoundParameters $PSBoundParameters
            $ConnectionInfo   = Connect-TntGraphSession @ConnectionParams

            # Initialize collections
            $PIMEligibleAssignments = [System.Collections.Generic.List[PSObject]]::new()
            $PIMActiveAssignments   = [System.Collections.Generic.List[PSObject]]::new()

            # Get all role definitions to identify privileged roles
            Write-Verbose 'Retrieving role definitions...'
            $RoleDefinitions = Get-MgRoleManagementDirectoryRoleDefinition -All -ErrorAction Stop
            # Client-side filtering required: Graph API does not support filtering by DisplayName array or IsBuiltIn property
            $PrivilegedRoles = $RoleDefinitions | Where-Object {
                $_.DisplayName -in $script:PrivilegedRoleNames -or $_.IsBuiltIn -eq $false
            }

            Write-Verbose "Identified $($PrivilegedRoles.Count) privileged roles"

            # Get PIM eligible assignments
            Write-Verbose 'Retrieving PIM eligible assignments...'
            try {
                $EligibleSchedules = Get-MgRoleManagementDirectoryRoleEligibilitySchedule -All -ExpandProperty Principal -ErrorAction Stop

                foreach ($Schedule in $EligibleSchedules) {
                    $Role = $PrivilegedRoles | Where-Object { $_.Id -eq $Schedule.RoleDefinitionId }
                    if ($Role) {
                        $Assignment = ConvertTo-PIMAssignment -Schedule $Schedule -Role $Role -AssignmentType 'PIM Eligible'
                        $PIMEligibleAssignments.Add($Assignment)
                    }
                }
            } catch {
                # Check if error is due to missing P2 license or permissions
                if ($_.Exception.Message -match 'Forbidden|Unauthorized|Premium|P2') {
                    $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                        [System.Exception]::new('Get-TntPIMReport failed: This feature requires Azure AD Premium P2 license.', $_.Exception),
                        'GetPIMReportLicenseError',
                        [System.Management.Automation.ErrorCategory]::InvalidOperation,
                        $TenantId
                    )
                    $PSCmdlet.ThrowTerminatingError($errorRecord)
                } else {
                    $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                        [System.Exception]::new("Get-TntPIMReport failed retrieving eligible assignments: $($_.Exception.Message)", $_.Exception),
                        'GetPIMReportEligibleError',
                        [System.Management.Automation.ErrorCategory]::OperationStopped,
                        $TenantId
                    )
                    $PSCmdlet.ThrowTerminatingError($errorRecord)
                }
            }

            # Get PIM active assignments
            Write-Verbose 'Retrieving PIM active assignments...'
            try {
                $ActiveSchedules = Get-MgRoleManagementDirectoryRoleAssignmentSchedule -All -ExpandProperty Principal -ErrorAction Stop

                foreach ($Schedule in $ActiveSchedules) {
                    $Role = $PrivilegedRoles | Where-Object { $_.Id -eq $Schedule.RoleDefinitionId }
                    if ($Role) {
                        $Assignment = ConvertTo-PIMAssignment -Schedule $Schedule -Role $Role -AssignmentType 'PIM Active'
                        $PIMActiveAssignments.Add($Assignment)
                    }
                }
            } catch {
                # Check if error is due to missing P2 license or permissions
                if ($_.Exception.Message -match 'Forbidden|Unauthorized|Premium|P2') {
                    $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                        [System.Exception]::new('Get-TntPIMReport failed: This feature requires Azure AD Premium P2 license.', $_.Exception),
                        'GetPIMReportLicenseError',
                        [System.Management.Automation.ErrorCategory]::InvalidOperation,
                        $TenantId
                    )
                    $PSCmdlet.ThrowTerminatingError($errorRecord)
                } else {
                    $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                        [System.Exception]::new("Get-TntPIMReport failed retrieving active assignments: $($_.Exception.Message)", $_.Exception),
                        'GetPIMReportActiveError',
                        [System.Management.Automation.ErrorCategory]::OperationStopped,
                        $TenantId
                    )
                    $PSCmdlet.ThrowTerminatingError($errorRecord)
                }
            }

            # Calculate summary statistics
            $TotalPIMAssignments = $PIMEligibleAssignments.Count + $PIMActiveAssignments.Count
            $UniqueEligibleUsers = ($PIMEligibleAssignments | Where-Object { $_.PrincipalType -eq 'user' } | Select-Object PrincipalId -Unique).Count

            # Generate summary
            $Summary = [PSCustomObject]@{
                TenantId                     = $TenantId
                ReportGeneratedDate          = Get-Date
                TotalPIMAssignments          = $TotalPIMAssignments
                PIMEligibleAssignments       = $PIMEligibleAssignments.Count
                PIMActiveAssignments         = $PIMActiveAssignments.Count
                UniqueEligibleUsers          = $UniqueEligibleUsers
                EligibleGlobalAdministrators = ($PIMEligibleAssignments | Where-Object { $_.RoleName -eq 'Global Administrator' }).Count
                ActiveGlobalAdministrators   = ($PIMActiveAssignments | Where-Object { $_.RoleName -eq 'Global Administrator' }).Count
            }

            # Build comprehensive report
            Write-Information "PIM report completed - $($TotalPIMAssignments) total assignments ($($PIMEligibleAssignments.Count) eligible, $($PIMActiveAssignments.Count) active)" -InformationAction Continue

            [PSCustomObject]@{
                Summary                = $Summary
                PIMEligibleAssignments = $PIMEligibleAssignments | Sort-Object RoleName, PrincipalName
                PIMActiveAssignments   = $PIMActiveAssignments | Sort-Object ExpirationDateTime, RoleName
                AssignmentsByRole      = ($PIMEligibleAssignments + $PIMActiveAssignments) | Group-Object RoleName | ForEach-Object {
                    [PSCustomObject]@{
                        RoleName      = $_.Name
                        EligibleCount = ($_.Group | Where-Object { $_.AssignmentType -eq 'PIM Eligible' }).Count
                        ActiveCount   = ($_.Group | Where-Object { $_.AssignmentType -eq 'PIM Active' }).Count
                    }
                } | Sort-Object { $_.EligibleCount + $_.ActiveCount } -Descending
            }
        } catch {
            $errorRecord = [System.Management.Automation.ErrorRecord]::new(
                [System.Exception]::new("Get-TntPIMReport failed: $($_.Exception.Message)", $_.Exception),
                'GetPIMReportError',
                [System.Management.Automation.ErrorCategory]::OperationStopped,
                $TenantId
            )
            $PSCmdlet.ThrowTerminatingError($errorRecord)
        } finally {
            if ($ConnectionInfo.ShouldDisconnect) {
                Disconnect-TntGraphSession -ConnectionState $ConnectionInfo
            }
        }
    }
}