Private/Add-IssueToPrincipal.ps1

function Add-IssueToPrincipal {
    <#
        .SYNOPSIS
        Adds Issue objects as properties to AD principal objects (users, groups, computers).
 
        .DESCRIPTION
        This function takes AD principal objects (DirectoryEntry or similar) and attaches
        security issues where they are the affected principal. Unlike Add-Issue which
        focuses on AD CS objects, this function focuses on the principals themselves.
 
        .PARAMETER Principals
        Array of AD principal objects (users, groups, computers) to attach issues to.
 
        .PARAMETER Issues
        Array of ESCalatorIssue objects where these principals are mentioned.
        Supports multiple arrays that will be automatically flattened.
 
        .INPUTS
        System.DirectoryServices.DirectoryEntry[]
        ESCalatorIssue[]
        ESCalatorIssue objects from Find-ESC4Issue, Find-ESC5Issue, or other vulnerability scanning functions.
        Supports multiple arrays that will be automatically flattened.
 
        .OUTPUTS
        System.DirectoryServices.DirectoryEntry[]
        Returns the principal objects with additional issue-related properties.
 
        .EXAMPLE
        $AllPrincipals = Get-IndividualPrincipals -Issues $AllIssues
        $PrincipalsWithIssues = Add-PrincipalIssue -Principals $AllPrincipals -Issues $AllIssues
 
        .EXAMPLE
        # Get specific users and attach their issues
        $Users = Get-ADUser -Filter * | Get-ADObject
        $UsersWithIssues = Add-PrincipalIssue -Principals $Users -Issues $AllExpandedIssues
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [object[]]$Principals,
        
        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [object[]]$Issues
    )


    begin {
        Write-Verbose "Starting principal issue attachment..."
        
        # Load ESCalatorIssue class if not already loaded
        if (-not ([System.Management.Automation.PSTypeName]'ESCalatorIssue').Type) {
            $escalatorIssuePath = Join-Path $PSScriptRoot "ESCalatorIssue.ps1"
            if (Test-Path $escalatorIssuePath) {
                . $escalatorIssuePath
            } else {
                throw "ESCalatorIssue class not found. Please ensure ESCalatorIssue.ps1 is available."
            }
        }
        
        # Flatten any nested arrays and validate all items are ESCalatorIssue objects
        $AllIssues = @()
        $NonESCalatorIssues = @()
        
        $Issues | ForEach-Object { 
            if ($_.PSObject.TypeNames[0] -eq 'ESCalatorIssue') { 
                $AllIssues += $_ 
            } elseif ($_ -is [Array]) {
                # Recursively flatten nested arrays
                $_ | ForEach-Object { 
                    if ($_.PSObject.TypeNames[0] -eq 'ESCalatorIssue') { 
                        $AllIssues += $_ 
                    } else {
                        $NonESCalatorIssues += $_
                    }
                }
            } else {
                $NonESCalatorIssues += $_
            }
        }
        
        # Warn about non-ESCalatorIssue objects but continue processing
        if ($NonESCalatorIssues.Count -gt 0) {
            Write-Warning "Found $($NonESCalatorIssues.Count) non-ESCalatorIssue objects that will be ignored. Expected ESCalatorIssue objects."
        }
        
        Write-Verbose "Processing $($AllIssues.Count) ESCalatorIssue objects for principal attachment"
    }

    process {
        foreach ($Principal in $Principals) {
            $principalName = if ($Principal.samAccountName.Value) { 
                $Principal.samAccountName.Value 
            } elseif ($Principal.samAccountName) { 
                $Principal.samAccountName 
            } else { 
                $Principal.Name 
            }
            
            $principalSID = if ($Principal.objectSid.Value) {
                (New-Object System.Security.Principal.SecurityIdentifier($Principal.objectSid.Value, 0)).Value
            } elseif ($Principal.Sid) {
                $Principal.Sid.Value
            } else {
                $null
            }

            # Find issues where this principal is involved
            $principalIssues = $AllIssues | Where-Object { 
                $_.IdentityReference -eq $principalName -or 
                $_.IdentityReferenceSID -eq $principalSID
            }

            # Categorize issues
            $directIssues = $principalIssues | Where-Object { -not $_.ExpandedFromGroup }
            $inheritedIssues = $principalIssues | Where-Object { $_.ExpandedFromGroup }
            
            # Calculate impact metrics
            $affectedObjects = $principalIssues | Select-Object -ExpandProperty Name -Unique
            $techniques = $principalIssues | Select-Object -ExpandProperty Technique -Unique
            $inheritedFromGroups = $inheritedIssues | Select-Object -ExpandProperty ExpandedFromGroup -Unique

            # Add properties specific to principals
            $Principal | Add-Member -NotePropertyName "SecurityIssues" -NotePropertyValue $principalIssues -Force
            $Principal | Add-Member -NotePropertyName "IssueCount" -NotePropertyValue $principalIssues.Count -Force
            $Principal | Add-Member -NotePropertyName "HasIssues" -NotePropertyValue ($principalIssues.Count -gt 0) -Force
            $Principal | Add-Member -NotePropertyName "DirectIssues" -NotePropertyValue $directIssues -Force
            $Principal | Add-Member -NotePropertyName "InheritedIssues" -NotePropertyValue $inheritedIssues -Force
            $Principal | Add-Member -NotePropertyName "DirectIssueCount" -NotePropertyValue $directIssues.Count -Force
            $Principal | Add-Member -NotePropertyName "InheritedIssueCount" -NotePropertyValue $inheritedIssues.Count -Force
            $Principal | Add-Member -NotePropertyName "AffectedObjects" -NotePropertyValue $affectedObjects -Force
            $Principal | Add-Member -NotePropertyName "AffectedObjectCount" -NotePropertyValue $affectedObjects.Count -Force
            $Principal | Add-Member -NotePropertyName "VulnerableTechniques" -NotePropertyValue $techniques -Force
            $Principal | Add-Member -NotePropertyName "InheritedFromGroups" -NotePropertyValue $inheritedFromGroups -Force
            
            # Principal-specific methods
            $Principal | Add-Member -MemberType ScriptMethod -Name "GetIssuesByObject" -Value {
                param([string]$ObjectName)
                return $this.SecurityIssues | Where-Object { $_.Name -eq $ObjectName }
            } -Force

            $Principal | Add-Member -MemberType ScriptMethod -Name "GetPrincipalSummary" -Value {
                $name = if ($this.samAccountName.Value) { $this.samAccountName.Value } else { $this.samAccountName }
                return [PSCustomObject]@{
                    PrincipalName = $name
                    PrincipalType = $this.objectClass -join ','
                    TotalIssues = $this.IssueCount
                    DirectIssues = $this.DirectIssueCount
                    InheritedIssues = $this.InheritedIssueCount
                    AffectedObjects = $this.AffectedObjectCount
                    InheritedFromGroups = $this.InheritedFromGroups -join ', '
                }
            } -Force

            Write-Output $Principal
        }
    }

    end {
        Write-Verbose "Completed principal issue attachment"
    }
}