Public/Find-LS2RiskyPrincipal.ps1

function Find-LS2RiskyPrincipal {
    <#
        .SYNOPSIS
        Analyzes principal-centric risk by aggregating vulnerabilities each principal can exploit.

        .DESCRIPTION
        Find-LS2RiskyPrincipal pivots from configuration-centric to principal-centric risk analysis.
        It expands group memberships and aggregates issues by individual principal, showing which
        users/service accounts have the highest exposure to AD CS vulnerabilities.
        
        This enables risk prioritization: remediate principals with the most exploitable paths first.
        
        The function automatically expands group issues into per-member issues, then aggregates by
        principal to show total exposure across all techniques and configurations.

        .PARAMETER Technique
        Filter results to only include a specific ESC technique (e.g., 'ESC1', 'ESC7a').
        If not specified, includes all techniques.

        .PARAMETER MinimumIssueCount
        Only return principals with at least this many exploitable issues.
        Default: 1 (show all principals with any exposure)

        .PARAMETER Top
        Return only the top N principals with highest risk exposure.
        If not specified, returns all principals matching criteria.

        .PARAMETER Rescan
        Forces a fresh vulnerability scan even if IssueStore is already populated.
        Clears the IssueStore and rescans all AD CS configurations.

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

        .OUTPUTS
        PSCustomObject
        Returns objects with the following properties:
        - Principal: NTAccount name of the principal
        - IssueCount: Total number of exploitable configurations
        - Techniques: Array of unique ESC techniques the principal can abuse
        - VulnerableObjects: Array of unique vulnerable objects (templates, CAs, etc.)
        - Issues: Hashtable of LS2Issue objects keyed by GetIdentifier() for detailed analysis

        .EXAMPLE
        Find-LS2RiskyPrincipal -Top 10
        Shows the 10 principals with the highest number of exploitable vulnerabilities.

        .EXAMPLE
        Find-LS2RiskyPrincipal -Technique ESC1 -MinimumIssueCount 5
        Shows principals who can abuse at least 5 ESC1 (SAN abuse) configurations.

        .EXAMPLE
        Find-LS2RiskyPrincipal | Where-Object Principal -like '*admin*'
        Shows risk exposure for all principals with 'admin' in their name.

        .EXAMPLE
        $topRisk = Find-LS2RiskyPrincipal -Top 20
        $topRisk | Format-Table Principal, IssueCount, Techniques
        Gets top 20 riskiest principals and displays summary information.

        .EXAMPLE
        Find-LS2RiskyPrincipal -Top 5 | ForEach-Object {
            "$($_.Principal): $($_.IssueCount) issues"
            $_.Issues.Values | Format-Table Technique, Name
        }
        Shows detailed breakdown of issues for top 5 riskiest principals.

        .NOTES
        Author: Jake Hildreth (@jakehildreth)
        Requires: PowerShell 5.1+
        
        If IssueStore is not already populated, this function will automatically run a full
        vulnerability scan (all techniques) to populate it. For better performance, run
        Invoke-Locksmith2 first if you plan to query multiple times.
        
        This function performs group expansion, which requires LDAP queries. For large environments
        with many group memberships, this may take time to complete.

        .LINK
        Invoke-Locksmith2

        .LINK
        Expand-IssueByGroup

        .LINK
        Get-FlattenedIssues
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter()]
        [ValidateSet('ESC1', 'ESC2', 'ESC3c1', 'ESC3c2', 'ESC4a', 'ESC4o', 'ESC5a', 'ESC5o', 'ESC6', 'ESC7a', 'ESC7m', 'ESC9', 'ESC11', 'ESC16')]
        [string]$Technique,
        
        [Parameter()]
        [int]$MinimumIssueCount = 1,
        
        [Parameter()]
        [int]$Top,
        
        [Parameter()]
        [switch]$Rescan
    )

    #requires -Version 5.1

    begin {
        # Ensure stores are initialized and populated
        $initParams = @{}
        if ($Rescan) { $initParams['Rescan'] = $true }
        
        if (-not (Initialize-LS2Scan @initParams)) {
            return
        }
        
        Write-Verbose "Retrieving all issues from IssueStore..."
    }

    process {
        # Get all flattened issues
        $allIssues = Get-FlattenedIssues
        
        if (-not $allIssues -or $allIssues.Count -eq 0) {
            Write-Warning "No issues found in IssueStore."
            return
        }
        
        Write-Verbose "Found $($allIssues.Count) total issue(s)"
        
        # Expand group memberships
        Write-Verbose "Expanding group memberships to individual principals..."
        $expandedIssues = $allIssues | ForEach-Object { Expand-IssueByGroup $_ }
        Write-Verbose "Expanded to $($expandedIssues.Count) issue(s) after group expansion"
        
        # Filter by technique if specified
        if ($Technique) {
            Write-Verbose "Filtering to technique: $Technique"
            $expandedIssues = $expandedIssues | Where-Object Technique -eq $Technique
            Write-Verbose "Filtered to $($expandedIssues.Count) issue(s) for $Technique"
        }
        
        # Filter to only permission-based issues (those with IdentityReference)
        $principalIssues = $expandedIssues | Where-Object { -not [string]::IsNullOrEmpty($_.IdentityReference) }
        Write-Verbose "Aggregating $($principalIssues.Count) permission-based issue(s) by principal..."
        
        # Group by principal and create risk report
        $riskReport = $principalIssues |
            Group-Object IdentityReference |
            ForEach-Object {
                $principalName = $_.Name
                $issues = $_.Group
                $issueCount = $issues.Count
                
                # Create hashtable of issues keyed by GetIdentifier()
                $issueHashtable = @{}
                foreach ($issue in $issues) {
                    $identifier = $issue.GetIdentifier()
                    if (-not $issueHashtable.ContainsKey($identifier)) {
                        $issueHashtable[$identifier] = $issue
                    }
                }
                
                [PSCustomObject]@{
                    Principal         = $principalName
                    IssueCount        = $issueCount
                    Techniques        = @($issues.Technique | Select-Object -Unique | Sort-Object)
                    VulnerableObjects = @($issues.Name | Select-Object -Unique | Sort-Object)
                    Issues            = $issueHashtable
                }
            } |
            Where-Object IssueCount -ge $MinimumIssueCount |
            Sort-Object IssueCount -Descending
        
        Write-Verbose "Risk report generated for $($riskReport.Count) principal(s)"
        
        # Return top N if specified
        if ($Top) {
            Write-Verbose "Returning top $Top principal(s)"
            $riskReport | Select-Object -First $Top
        } else {
            $riskReport
        }
    }
}