Private/Expand-Issue.ps1
|
function Expand-Issue { <# .SYNOPSIS Expands ESCalatorIssue objects by recursively enumerating group members when the identity is a group. .DESCRIPTION This function takes ESCalatorIssue objects and checks if the IdentityReference represents a group. If it's a group, the function recursively enumerates all group members and creates individual ESCalatorIssue objects for each principal (user/computer) in the group. Non-group identities are returned unchanged. .PARAMETER Issue ESCalatorIssue objects to expand. If the IdentityReference is a group, it will be expanded to individual member principals. .PARAMETER Recursive Whether to recursively expand nested groups. Default is $true. .INPUTS ESCalatorIssue[] ESCalatorIssue objects with IdentityReference properties to expand. .OUTPUTS ESCalatorIssue[] Returns expanded ESCalatorIssue objects with individual principals instead of groups, plus any non-group issues unchanged. .EXAMPLE $Issues = Find-ESC4Issue -AdcsObjects $AdcsObjects $ExpandedIssues = $Issues | Expand-Issue $ExpandedIssues | Where-Object { $_.IsExpanded() } | Format-Table .EXAMPLE $ESC5Issues = Find-ESC5Issue -AdcsObjects $AdcsObjects $AllExpanded = $ESC5Issues | Expand-Issue -Recursive $true Write-Host "Original issues: $($ESC5Issues.Count), Expanded: $($AllExpanded.Count)" .EXAMPLE # Expand only specific group issues $GroupIssues = $AllIssues | Where-Object { $_.IdentityReferenceSID -match '^S-1-5-.*-5[0-9][0-9]$' } $ExpandedGroupIssues = $GroupIssues | Expand-Issue .LINK https://posts.specterops.io/certified-pre-owned-d95910965cd2 #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] $Issue, [Parameter()] [bool]$Recursive = $true ) begin { Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Starting $($MyInvocation.MyCommand) on $env:COMPUTERNAME..." Add-Type -AssemblyName 'System.DirectoryServices.AccountManagement' # 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." } } } process { Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Processing $($Issue.Count) issue(s) for group expansion..." # Handle array flattening - support multiple array syntax like ($Array1, $Array2) $FlattenedIssues = @() foreach ($IssueSet in $Issue) { if ($null -ne $IssueSet) { # Check if this is a single ESCalatorIssue object if ($IssueSet.PSObject.TypeNames[0] -eq 'ESCalatorIssue') { $FlattenedIssues += $IssueSet } # Check if this is an array (could be array of ESCalatorIssue or nested arrays) elseif ($IssueSet -is [System.Array] -or $IssueSet -is [System.Collections.IEnumerable]) { foreach ($SubItem in $IssueSet) { if ($null -ne $SubItem) { # Recursively handle nested arrays if ($SubItem.PSObject.TypeNames[0] -eq 'ESCalatorIssue') { $FlattenedIssues += $SubItem } elseif ($SubItem -is [System.Array] -or $SubItem -is [System.Collections.IEnumerable]) { foreach ($NestedItem in $SubItem) { if ($null -ne $NestedItem -and $NestedItem.PSObject.TypeNames[0] -eq 'ESCalatorIssue') { $FlattenedIssues += $NestedItem } else { Write-Warning "Skipping non-ESCalatorIssue object in nested array: $($NestedItem.GetType().Name)" } } } else { Write-Warning "Skipping non-ESCalatorIssue object in array: $($SubItem.GetType().Name)" } } } } else { Write-Warning "Skipping non-ESCalatorIssue object: $($IssueSet.GetType().Name)" } } } Write-Verbose "Processing $($FlattenedIssues.Count) flattened issues for expansion..." foreach ($IssueObject in $FlattenedIssues) { Write-Verbose "Processing issue for $($IssueObject.Name) - Identity: $($IssueObject.IdentityReference)" try { # Skip if this is already an expanded issue to avoid infinite recursion if ($IssueObject.IsExpanded()) { Write-Verbose "Issue is already expanded from group: $($IssueObject.ExpandedFromGroup), skipping" Write-Output $IssueObject continue } # Extract domain from the forest or try to determine from SID $domain = $IssueObject.Forest if (-not $domain -or $domain -eq 'Unknown') { # Try to extract domain from DN if available if ($IssueObject.DistinguishedName) { $parts = $IssueObject.DistinguishedName -split ',DC=' if ($parts.Count -gt 1) { $domain = $parts[1..($parts.Count-1)] -join '.' } } } # Try to resolve the SID to determine if it's a group $sid = $IssueObject.IdentityReferenceSID if ($sid -match '^S-1-') { Write-Verbose "Attempting to resolve SID: $sid in domain: $domain" $PrincipalContext = $null $Principal = $null $GroupPrincipal = $null try { $PrincipalContext = [System.DirectoryServices.AccountManagement.PrincipalContext]::New('Domain', $domain) $Principal = [System.DirectoryServices.AccountManagement.Principal]::FindByIdentity($PrincipalContext, 'Sid', $sid) if ($Principal -and $Principal.GetType().Name -eq 'GroupPrincipal') { Write-Verbose "Found group: $($Principal.Name), expanding members..." # Get group members $GroupPrincipal = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($PrincipalContext, 'Sid', $sid) if ($GroupPrincipal) { $members = if ($Recursive) { $GroupPrincipal.GetMembers($true) # Recursive expansion } else { $GroupPrincipal.GetMembers() # Direct members only } if ($members) { $memberCount = 0 foreach ($member in $members) { $memberCount++ Write-Verbose "Expanding member $memberCount : $($member.SamAccountName) ($($member.GetType().Name))" # Construct domain\username format consistent with original issues # Extract NetBIOS domain name from the original issue's IdentityReference $netbiosDomain = if ($IssueObject.IdentityReference -match '^([^\\]+)\\') { $matches[1] } elseif ($member.Context.Name) { # Fallback: try to get NetBIOS name from context $member.Context.Name } elseif ($domain) { # Last resort: use first part of FQDN $domain.Split('.')[0] } else { $null } $memberIdentity = if ($netbiosDomain) { "$netbiosDomain\$($member.SamAccountName)" } else { $member.SamAccountName } # Create expanded issue using ESCalatorIssue class method $expandedIssue = [ESCalatorIssue]::CreateExpandedIssue( $IssueObject.Forest, # Forest $IssueObject.Name, # Name $IssueObject.DistinguishedName, # DistinguishedName $memberIdentity, # IdentityReference (with NetBIOS domain) $member.Sid.Value, # IdentityReferenceSID $IssueObject.ActiveDirectoryRights, # ActiveDirectoryRights $IssueObject.Technique, # Technique $IssueObject.Subtype, # Subtype ($IssueObject.Issue -replace [regex]::Escape($IssueObject.IdentityReference), $memberIdentity), # Issue (updated with NetBIOS format) $IssueObject.Severity, # Severity $IssueObject.ObjectType, # ObjectType $IssueObject.DirectoryEntry, # DirectoryEntry $IssueObject.IdentityReference, # ExpandedFromGroup $IssueObject.IdentityReferenceSID, # ExpandedFromGroupSID $member.GetType().Name # MemberType ) Write-Output $expandedIssue } Write-Verbose "Successfully expanded group $($Principal.Name) into $memberCount members" } else { Write-Verbose "Group $($Principal.Name) has no members" # Output original issue with note that group is empty $emptyGroupIssue = $IssueObject.CreateCopy(@{}) $emptyGroupIssue | Add-Member -NotePropertyName "GroupExpansionNote" -NotePropertyValue "Group has no members" -Force Write-Output $emptyGroupIssue } } else { Write-Warning "Could not cast principal to GroupPrincipal for SID: $sid" Write-Output $IssueObject } } else { Write-Verbose "SID $sid is not a group or could not be resolved, outputting original issue" # Not a group, output original issue Write-Output $IssueObject } } catch { Write-Warning "Failed to expand group membership for SID $sid in domain $domain : $_" # Output original issue with error note $errorIssue = $IssueObject.CreateCopy(@{}) $errorIssue | Add-Member -NotePropertyName "GroupExpansionError" -NotePropertyValue $_.Exception.Message -Force Write-Output $errorIssue } finally { # Clean up resources if ($GroupPrincipal) { $GroupPrincipal.Dispose() } if ($Principal) { $Principal.Dispose() } if ($PrincipalContext) { $PrincipalContext.Dispose() } } } else { Write-Verbose "IdentityReferenceSID '$sid' is not a valid SID format, outputting original issue" Write-Output $IssueObject } } catch { Write-Warning "Failed to process issue object for $($IssueObject.Name): $_" Write-Output $IssueObject } } } end { Write-Verbose "[$(Get-Date -Format 'yyyy-MM-dd hh:mm:ss')] Finishing $($MyInvocation.MyCommand) on $env:COMPUTERNAME..." } } |