Public/Discovery/Get-PrivilegedApp.ps1

function Get-PrivilegedApp {
    [cmdletbinding()]
    param (
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $false)]
        [int]$ThrottleLimit = 1000,

        [Parameter(Mandatory = $false)]
        [ValidateSet("Object", "JSON", "CSV", "Table")]
        [Alias("output", "o")]
        [string]$OutputFormat = "Table",

        [Parameter(Mandatory = $false)]
        [Alias("include-owners", "owners")]
        [switch]$IncludeOwners
    )

    begin {
        Write-Verbose "🚀 Starting function $($MyInvocation.MyCommand.Name)"
        $MyInvocation.MyCommand.Name | Invoke-BlackCat -ResourceTypeName "MSGraph"

        $result = New-Object System.Collections.ArrayList
        $stats = @{ 
            StartTime         = Get-Date
            TotalApplications = 0
        }
        
        # Thread-safe counters for parallel processing
        $privilegedAppCount = [ref]0
        $credentialAppCount = [ref]0
        $keyCredentialAppCount = [ref]0
        $expiredCredentialAppCount = [ref]0
        $expiredKeyCredentialAppCount = [ref]0
    }

    process {
        try {
            Write-Message -FunctionName $MyInvocation.MyCommand.Name -Message "🔍 Collecting Enterprise Applications" -Severity 'Information'
            $applications = Invoke-MsGraph -relativeUrl "applications"

            $stats.TotalApplications = $applications.count
            Write-Verbose "📱 User Applications: $($applications.count)"
            Write-Verbose " 🔐 Validating [$($applications.count)] Enterprise Applications for privileged permissions"

            $riskyGrants = $sessionVariables.appRoleIds | Where-Object Permission -in @(
                'Directory.ReadWrite.All',
                'PrivilegedAccess.ReadWrite.AzureAD',
                'PrivilegedAccess.ReadWrite.AzureADGroup',
                'PrivilegedAccess.ReadWrite.AzureResources',
                'Policy.ReadWrite.ConditionalAccess',
                'GroupMember.ReadWrite.All',
                'Group.ReadWrite.All',
                'RoleManagement.ReadWrite.Directory',
                'Application.ReadWrite.All'
            )

            Write-Verbose " 🔍 Pre-filtering applications with privileged permissions..."
            $privilegedApps = $applications | Where-Object {
                $app = $_
                $hasPrivilegedPermissions = $false
                foreach ($riskyGrant in $riskyGrants) {
                    if ($app.requiredResourceAccess.resourceAccess.id -contains $riskyGrant.appRoleId) {
                        $hasPrivilegedPermissions = $true
                        break
                    }
                }
                $hasPrivilegedPermissions
            }

            Write-Verbose " 📊 Found $($privilegedApps.Count) applications with privileged permissions (filtering from $($applications.count) total)"

            # Only fetch owners if explicitly requested or if output format needs detailed information
            $ownerLookup = @{}
            if ($IncludeOwners) {
                Write-Verbose " 👥 Fetching owners for privileged applications..."
                foreach ($app in $privilegedApps) {
                    try {
                        $owners = Invoke-MsGraph -relativeUrl "applications/$($app.Id)/owners"
                        $ownerLookup[$app.Id] = $owners.userPrincipalName
                    }
                    catch {
                        Write-Verbose " ⚠️ Failed to get owners for application $($app.Id): $($_.Exception.Message)"
                        $ownerLookup[$app.Id] = @()
                    }
                }
            }
            else {
                Write-Verbose " ⚡ Skipping owner lookup for better performance (use -IncludeOwners to fetch owners)"
                # Initialize empty lookup for all privileged apps
                foreach ($app in $privilegedApps) {
                    $ownerLookup[$app.Id] = @("Not fetched - use -IncludeOwners parameter")
                }
            }

            $privilegedApps | ForEach-Object -Parallel {
                $riskyGrants = $using:riskyGrants
                $result = $using:result
                $ownerLookup = $using:ownerLookup
                $privilegedAppCount = $using:privilegedAppCount
                $credentialAppCount = $using:credentialAppCount
                $keyCredentialAppCount = $using:keyCredentialAppCount
                $expiredCredentialAppCount = $using:expiredCredentialAppCount
                $expiredKeyCredentialAppCount = $using:expiredKeyCredentialAppCount

                $permissionObjects = @()

                foreach ($riskyGrant in $riskyGrants) {
                    if ($_.requiredResourceAccess.resourceAccess.id -contains $riskyGrant.appRoleId) {
                        $permissionObjects += $riskyGrant.Permission
                    }
                }

                if ($permissionObjects.Count -gt 0) {
                    # Determine severity level based on permissions (ordered by risk level)
                    $severity = "Low"  # Default for any other risky permissions
                    
                    # Critical - Permissions that provide broad administrative access or bypass security controls
                    if ($permissionObjects -contains "Directory.ReadWrite.All" -or
                        $permissionObjects -contains "PrivilegedAccess.ReadWrite.AzureAD" -or
                        $permissionObjects -contains "PrivilegedAccess.ReadWrite.AzureADGroup" -or
                        $permissionObjects -contains "PrivilegedAccess.ReadWrite.AzureResources" -or
                        $permissionObjects -contains "Policy.ReadWrite.ConditionalAccess") { 
                        $severity = "Critical" 
                    }
                    # High - Permissions that can escalate privileges or manage credentials
                    elseif ($permissionObjects -contains "RoleManagement.ReadWrite.Directory" -or
                            $permissionObjects -contains "Application.ReadWrite.All" -or
                            $permissionObjects -contains "GroupMember.ReadWrite.All" -or
                            $permissionObjects -contains "Group.ReadWrite.All") { 
                        $severity = "High" 
                    }
                    # Medium severity is now handled by the default "Low" since these are all high-risk permissions

                    $currentItem = [PSCustomObject]@{
                        DisplayName              = $_.DisplayName
                        Id                       = $_.Id
                        Severity                 = $severity
                        CreatedDateTime          = $_.CreatedDateTime
                        Permission               = $permissionObjects | Sort-Object -Unique
                        Owners                   = $ownerLookup[$_.Id]
                        HasCredentials           = $false
                        HasKeyCredentials        = $false
                        PasswordCredentialExpiry = $null
                        KeyCredentialExpiry      = $null
                    }

                    if ($_.PasswordCredentials.KeyId) {
                        $currentItem | Add-Member -MemberType NoteProperty -Name Credentials -Value $_.PasswordCredentials -Force
                        $currentItem.HasCredentials = $true
                        
                        $passwordExpiry = $_.PasswordCredentials | Where-Object { $_.EndDateTime } | 
                        Sort-Object EndDateTime | Select-Object -First 1 -ExpandProperty EndDateTime
                        if ($passwordExpiry) {
                            $currentItem.PasswordCredentialExpiry = [DateTime]$passwordExpiry

                            if ([DateTime]$passwordExpiry -lt (Get-Date)) {
                                [void][System.Threading.Interlocked]::Increment($expiredCredentialAppCount)
                            }
                        }
                        
                        [void][System.Threading.Interlocked]::Increment($credentialAppCount)
                    }

                    if ($_.KeyCredentials.Value) {
                        $currentItem | Add-Member -MemberType NoteProperty -Name KeyCredentials -Value $_.KeyCredentials -Force
                        $currentItem.HasKeyCredentials = $true
                        
                        $keyExpiry = $_.KeyCredentials | Where-Object { $_.EndDateTime } | 
                        Sort-Object EndDateTime | Select-Object -First 1 -ExpandProperty EndDateTime
                        if ($keyExpiry) {
                            $currentItem.KeyCredentialExpiry = [DateTime]$keyExpiry

                            if ([DateTime]$keyExpiry -lt (Get-Date)) {
                                [void][System.Threading.Interlocked]::Increment($expiredKeyCredentialAppCount)
                            }
                        }
                        
                        [void][System.Threading.Interlocked]::Increment($keyCredentialAppCount)
                    }

                    [void][System.Threading.Interlocked]::Increment($privilegedAppCount)
                    [void]$result.Add($currentItem)
                }
            } -ThrottleLimit $ThrottleLimit

            $json = [ordered]@{}
            [void]$json.Add("data", $result)

            Write-Message -FunctionName $MyInvocation.MyCommand.Name -Message "✅ Found $($result.Count) privileged applications" -Severity 'Information'
        }
        catch {
            Write-Message -FunctionName $MyInvocation.MyCommand.Name -Message "❌ $($_.Exception.Message)" -Severity 'Error'
        }
    }

    end {
        $Duration = (Get-Date) - $stats.StartTime
        
        # Update stats with final counts from thread-safe counters
        $stats.PrivilegedApplications = $privilegedAppCount.Value
        $stats.ApplicationsWithCredentials = $credentialAppCount.Value
        $stats.ApplicationsWithKeyCredentials = $keyCredentialAppCount.Value
        $stats.ApplicationsWithExpiredCredentials = $expiredCredentialAppCount.Value
        $stats.ApplicationsWithExpiredKeyCredentials = $expiredKeyCredentialAppCount.Value
        
        Write-Host "`n📊 Privileged Application Discovery Summary:" -ForegroundColor Magenta
        Write-Host " Total Applications Analyzed: $($stats.TotalApplications)" -ForegroundColor White
        Write-Host " Privileged Applications Found: $($stats.PrivilegedApplications)" -ForegroundColor Yellow
        Write-Host " Applications with Password Credentials: $($stats.ApplicationsWithCredentials)" -ForegroundColor Cyan
        Write-Host " Applications with Key Credentials: $($stats.ApplicationsWithKeyCredentials)" -ForegroundColor Cyan
        Write-Host " Applications with Expired Password Credentials: $($stats.ApplicationsWithExpiredCredentials)" -ForegroundColor Red
        Write-Host " Applications with Expired Key Credentials: $($stats.ApplicationsWithExpiredKeyCredentials)" -ForegroundColor Red
        Write-Host " Duration: $($Duration.TotalSeconds.ToString('F2')) seconds" -ForegroundColor White
        
        if ($result.Count -gt 0) {
            # Group by severity level for summary
            $severityLevelCounts = $result | Group-Object Severity | Sort-Object @{Expression = {
                    switch ($_.Name) {
                        "Critical" { 1 }
                        "High" { 2 }
                        "Medium" { 3 }
                        "Low" { 4 }
                    }
                }
            }
            
            Write-Host "`n Severity Level Breakdown:" -ForegroundColor White
            foreach ($group in $severityLevelCounts) {
                $emoji = switch ($group.Name) {
                    "Critical" { "🚨" }
                    "High" { "🔴" }
                    "Medium" { "🟠" }
                    "Low" { "⚠️" }
                }
                Write-Host " $emoji $($group.Name): $($group.Count)" -ForegroundColor White
            }
            
            # Return results in requested format
            switch ($OutputFormat) {
                "JSON" { 
                    $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
                    $jsonOutput = $result | ConvertTo-Json -Depth 3
                    $jsonFilePath = "PrivilegedApps_$timestamp.json"
                    $jsonOutput | Out-File -FilePath $jsonFilePath -Encoding UTF8
                    Write-Host "💾 JSON output saved to: $jsonFilePath" -ForegroundColor Green
                    # File created, no console output needed
                    return
                }
                "CSV" { 
                    $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
                    $csvOutput = $result | ConvertTo-CSV
                    $csvFilePath = "PrivilegedApps_$timestamp.csv"
                    $csvOutput | Out-File -FilePath $csvFilePath -Encoding UTF8
                    Write-Host "📊 CSV output saved to: $csvFilePath" -ForegroundColor Green
                    # File created, no console output needed
                    return
                }
                "Object" { return $result }
                "Table" { return $result | Format-Table -AutoSize }
            }
        }
        else {
            Write-Host "`n❌ No privileged applications found" -ForegroundColor Red
            Write-Information "No privileged applications found" -InformationAction Continue
        }
        
        Write-Verbose "🏁 Completed function $($MyInvocation.MyCommand.Name)"
    }
    <#
.SYNOPSIS
    Retrieves Microsoft Entra ID (Azure AD) applications with privileged permissions.
 
.DESCRIPTION
    The Get-PrivilegedApp function identifies and returns Enterprise Applications that have been granted high-risk
    permissions in Microsoft Entra ID. It specifically looks for applications with permissions such as Directory.ReadWrite.All,
    PrivilegedAccess.ReadWrite.AzureAD, and other sensitive permissions that could pose security risks.
 
.PARAMETER ThrottleLimit
    Specifies the maximum number of concurrent operations that can be performed in parallel.
    Default value is 1000.
 
.PARAMETER OutputFormat
    Optional. Specifies the output format for results. Valid values are:
    - Object: Returns PowerShell objects (default when piping)
    - JSON: Creates timestamped JSON file (PrivilegedApps_TIMESTAMP.json) with no console output
    - CSV: Creates timestamped CSV file (PrivilegedApps_TIMESTAMP.csv) with no console output
    - Table: Returns results in a formatted table (default)
    Aliases: output, o
 
.PARAMETER IncludeOwners
    Optional. When specified, fetches application owner information. This adds API calls and processing time
    but provides complete ownership details. Owners are automatically included for Object, JSON, and CSV formats.
    For Table format, use this switch to include owner information.
    Aliases: include-owners, owners
 
.OUTPUTS
    Returns an array of PSCustomObjects containing the following properties:
    - Id: The unique identifier of the application
    - DisplayName: The display name of the application
    - Severity: Risk level (Critical, High, Medium, Low) based on permissions
    - CreatedDateTime: When the application was created
    - Permission: Array of high-risk permissions granted to the application
    - Owners: List of application owners
    - HasCredentials: Boolean indicating if password credentials are present
    - HasKeyCredentials: Boolean indicating if certificate credentials are present
    - PasswordCredentialExpiry: Earliest expiry date of password credentials (if any)
    - KeyCredentialExpiry: Earliest expiry date of key/certificate credentials (if any)
    - Credentials: (Optional) If present, contains password credentials information
    - KeyCredentials: (Optional) If present, contains certificate credentials information
 
.EXAMPLE
    Get-PrivilegedApp
    Returns all applications with high-risk permissions using default throttle limit and table format.
    Owners are not fetched for optimal performance.
 
.EXAMPLE
    Get-PrivilegedApp -IncludeOwners
    Returns all applications with high-risk permissions and includes owner information.
 
.EXAMPLE
    Get-PrivilegedApp -ThrottleLimit 500
    Returns all applications with high-risk permissions using a custom throttle limit of 500.
 
.EXAMPLE
    Get-PrivilegedApp -Verbose
    Returns all applications with high-risk permissions with detailed progress information.
 
.EXAMPLE
    Get-PrivilegedApp -OutputFormat JSON
    Creates a timestamped JSON file (e.g., PrivilegedApps_20250629_143022.json) in the current directory.
    No console output is displayed; only file creation confirmation message is shown.
 
.EXAMPLE
    Get-PrivilegedApp -OutputFormat CSV -ThrottleLimit 200
    Creates a timestamped CSV file (e.g., PrivilegedApps_20250629_143022.csv) in the current directory.
    Uses a throttle limit of 200. No console output is displayed; only file creation confirmation message is shown.
 
.EXAMPLE
    Get-PrivilegedApp -IncludeOwners -OutputFormat Table
    Returns privileged applications in table format with owner information included.
 
.NOTES
    File: Get-PrivilegedApp.ps1
    Author: Script Author
    Version: 1.0
    Requires: PowerShell 7.0 or later
    Requires: Microsoft Graph API access
    Requires: Appropriate permissions to read application information
 
    The function checks for the following high-risk permissions and assigns severity levels:
 
    CRITICAL severity permissions (broad administrative access/bypass security controls):
    - Directory.ReadWrite.All
    - PrivilegedAccess.ReadWrite.AzureAD
    - PrivilegedAccess.ReadWrite.AzureADGroup
    - PrivilegedAccess.ReadWrite.AzureResources
    - Policy.ReadWrite.ConditionalAccess
 
    HIGH severity permissions (privilege escalation/credential management):
    - RoleManagement.ReadWrite.Directory
    - Application.ReadWrite.All
    - GroupMember.ReadWrite.All
    - Group.ReadWrite.All
 
#>

}