Private/Utility/Expand-IssueByGroup.ps1

function Expand-IssueByGroup {
    <#
        .SYNOPSIS
        Expands issues for group principals into individual issues for each group member.

        .DESCRIPTION
        Takes an LS2Issue object and checks if the IdentityReferenceSID belongs to a group.
        If it is a group, creates individual LS2Issue objects for each direct member of the group.
        If not a group, returns the original issue unchanged.
        
        This allows security findings to be attributed to individual users rather than just
        showing a group has dangerous permissions.

        .PARAMETER Issue
        The LS2Issue object to potentially expand.

        .PARAMETER IncludeGroup
        If specified, includes the original group issue in the output along with member issues.
        By default, only member issues are returned for groups.

        .OUTPUTS
        LS2Issue[]
        Returns an array of LS2Issue objects. For non-groups, returns a single-item array
        with the original issue. For groups, returns one issue per member (and optionally
        the group issue itself if -IncludeGroup is specified).

        .EXAMPLE
        $issue | Expand-IssueByGroup
        Expands group issues into per-member issues, omitting the group issue.

        .EXAMPLE
        $issue | Expand-IssueByGroup -IncludeGroup
        Expands group issues but also includes the original group issue in output.

        .EXAMPLE
        $allIssues = $issues | ForEach-Object { Expand-IssueByGroup $_ }
        Processes an array of issues, expanding any that reference groups.

        .NOTES
        Requires PrincipalStore to be populated with resolved principals including objectClass.
        Uses Expand-GroupMembership to get direct members of groups.
        The MemberCount property is set on group issues to indicate expansion occurred.
    #>

    [CmdletBinding()]
    [OutputType([LS2Issue[]])]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [LS2Issue]$Issue,
        
        [Parameter()]
        [switch]$IncludeGroup
    )

    #requires -Version 5.1

    process {
        # If no IdentityReferenceSID, this isn't a permission-based issue, return as-is
        if ([string]::IsNullOrEmpty($Issue.IdentityReferenceSID)) {
            Write-Verbose "Issue has no IdentityReferenceSID - returning unchanged"
            return @($Issue)
        }

        # Check if principal is a group
        $principal = $script:PrincipalStore[$Issue.IdentityReferenceSID]
        if (-not $principal) {
            Write-Verbose "Principal $($Issue.IdentityReferenceSID) not found in PrincipalStore - returning unchanged"
            return @($Issue)
        }

        $isGroup = $principal.objectClass -eq 'group'
        
        if (-not $isGroup) {
            Write-Verbose "Principal $($Issue.IdentityReference) is not a group - returning unchanged"
            return @($Issue)
        }

        Write-Verbose "Expanding group $($Issue.IdentityReference) into member issues"

        # Expand group membership
        $members = Expand-GroupMembership -SidList @($Issue.IdentityReferenceSID)
        
        # Filter out the group itself from members
        $memberSids = $members | Where-Object { $_ -ne $Issue.IdentityReferenceSID }
        
        if (-not $memberSids -or $memberSids.Count -eq 0) {
            Write-Verbose "Group $($Issue.IdentityReference) has no members - returning original issue"
            # Update MemberCount to 0
            $Issue.MemberCount = 0
            return @($Issue)
        }

        Write-Verbose "Found $($memberSids.Count) member(s) in group $($Issue.IdentityReference)"

        # Create array to hold results
        $expandedIssues = @()

        # Optionally include the original group issue
        if ($IncludeGroup) {
            # Update MemberCount on the group issue
            $Issue.MemberCount = $memberSids.Count
            $expandedIssues += $Issue
        }

        # Create an issue for each member
        foreach ($memberSid in $memberSids) {
            # Get member principal from store
            $memberPrincipal = $script:PrincipalStore[$memberSid]
            
            if (-not $memberPrincipal) {
                Write-Verbose "Member SID $memberSid not found in PrincipalStore - skipping"
                continue
            }

            $memberNTAccount = $memberPrincipal.NTAccountName
            
            Write-Verbose " Creating issue for member: $memberNTAccount"

            # Create issue description explaining group membership path
            $memberIssueText = "$memberNTAccount ($memberSid) is able to abuse this configuration via membership in the group $($Issue.IdentityReference) ($($Issue.IdentityReferenceSID))."
            
            # Create remediation reference pointing back to the original group issue
            $remediationReference = "For full remediation details, refer to $($Issue.GetIdentifier())"
            $fixText = "$memberNTAccount ($memberSid) is able to abuse this configuration via membership in the group $($Issue.IdentityReference) ($($Issue.IdentityReferenceSID)). $remediationReference"
            $revertText = $fixText

            # Clone the issue with new principal information
            $memberIssue = [LS2Issue]@{
                Technique             = $Issue.Technique
                Forest                = $Issue.Forest
                Name                  = $Issue.Name
                DistinguishedName     = $Issue.DistinguishedName
                IdentityReference     = $memberNTAccount
                IdentityReferenceSID  = $memberSid
                ActiveDirectoryRights = $Issue.ActiveDirectoryRights
                Enabled               = $Issue.Enabled
                EnabledOn             = $Issue.EnabledOn
                CAFullName            = $Issue.CAFullName
                Owner                 = $Issue.Owner
                HasNonStandardOwner   = $Issue.HasNonStandardOwner
                Issue                 = $memberIssueText
                Fix                   = $fixText
                Revert                = $revertText
            }

            $expandedIssues += $memberIssue
        }

        Write-Verbose "Expanded group $($Issue.IdentityReference) into $($expandedIssues.Count) issue(s)"
        
        return $expandedIssues
    }
}