Private/SID/Invoke-SIDProcessing.ps1

#Requires -Version 5.1

<#
.SYNOPSIS
    SID Processing Orchestration Module for Find-UnknownSID Solution
 
.DESCRIPTION
    This focused module orchestrates the entire orphaned SID detection workflow
    for the Find-UnknownSID enterprise solution. It coordinates the interaction
    between security descriptor retrieval, identity resolution, and result creation
    while managing processing statistics and error aggregation.
 
    SINGLE RESPONSIBILITY:
    This module does ONE thing well: Orchestrate the complete orphaned SID detection
    process by coordinating specialized modules and managing the overall workflow.
 
    BUSINESS VALUE:
    - Streamlined orchestration of complex multi-module workflows
    - Comprehensive error aggregation and processing statistics
    - Memory-efficient processing for large-scale AD environments
    - Standardized pipeline processing with correlation tracking
 
    TECHNICAL FEATURES:
    - Pipeline processing coordination with memory management
    - Error aggregation and graceful degradation
    - Processing statistics management and reporting
    - Integration with all specialized processing modules
    - Comprehensive logging and correlation tracking
 
.NOTES
    Author: Jeffrey Stuhr
    Blog: https://www.techbyjeff.net
    LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/
    Last Updated: 2025-01-27
    Version: 1.0.0
    PowerShell Version: 5.1+ (Windows), 7.x+ (Cross-platform)
 
    TROUBLESHOOTING:
    - For orchestration issues: .\Troubleshooting\Common\SID-Processing-Orchestration-Issues.md
    - For performance problems: .\Troubleshooting\Performance\SID-Processing-Performance.md
    - For memory management: .\Troubleshooting\Performance\Memory-Management-Issues.md
 
    DEPENDENCIES:
    - Requires Get-SecurityDescriptor.ps1 for ACL retrieval
    - Requires Resolve-SIDIdentity.ps1 for identity resolution
    - Requires New-SIDResult.ps1 for result object creation
    - Requires Logging.ps1 for Write-StructuredLog function
    - Uses script-scoped variables (CorrelationId for tracking)
#>


#region SID Processing Orchestration Functions

function Find-OrphanedSIDsInObject {
    <#
    .SYNOPSIS
        Orchestrates orphaned SID detection in Active Directory objects
 
    .DESCRIPTION
        Main orchestration function that coordinates the complete orphaned SID detection
        workflow across multiple specialized modules. This function manages the entire
        process from security descriptor retrieval through result object creation,
        providing comprehensive error handling and processing statistics.
 
        The orchestration workflow includes:
        - Input validation and error object filtering
        - Security descriptor retrieval using multiple strategies
        - Access rule processing and identity resolution
        - Orphaned SID detection and analysis
        - Result object creation and metadata population
        - Processing statistics and memory management
 
    .PARAMETER ADObject
        Active Directory objects to process. Can be from Get-ADObject output,
        background job results, or pipeline input. Objects must have
        DistinguishedName and nTSecurityDescriptor properties.
 
    .PARAMETER IncludeInherited
        When specified, includes inherited ACL entries in the orphaned SID analysis.
        By default, only explicit (non-inherited) ACL entries are processed.
 
    .PARAMETER CurrentDomainSID
        The SID of the current domain for context analysis. Used to categorize
        SIDs as local, foreign, or unknown domain objects.
 
    .PARAMETER CorrelationId
        Unique identifier for tracking this processing operation across logs
        and audit trails. Generated automatically if not provided.
 
    .EXAMPLE
        PS> $adObjects | Find-OrphanedSIDsInObject
 
        DESCRIPTION: Basic orphaned SID detection using pipeline processing
        OUTPUT: Array of OrphanedSIDResult objects for all orphaned SIDs found
        USE CASE: Standard security audit of AD objects from Get-ADObject
 
    .EXAMPLE
        PS> Find-OrphanedSIDsInObject -ADObject $adObjects -IncludeInherited -CurrentDomainSID $domainSID
 
        DESCRIPTION: Comprehensive analysis including inherited permissions and domain context
        OUTPUT: Complete orphaned SID analysis with enhanced categorization
        BUSINESS CASE: Thorough security audit including all permission sources
 
    .EXAMPLE
        PS> $jobResults | Find-OrphanedSIDsInObject -CorrelationId $correlationId
 
        DESCRIPTION: Processing background job results with correlation tracking
        OUTPUT: Orphaned SID results with full audit trail correlation
        INTEGRATION: Large-scale parallel processing with enterprise tracking
 
    .OUTPUTS
        [OrphanedSIDResult[]] Array of comprehensive result objects containing
        detailed information about discovered orphaned SIDs, including object
        context, analysis metadata, and processing information.
 
    .NOTES
        ORCHESTRATION WORKFLOW:
        1. Input validation and error object filtering
        2. Security descriptor retrieval (delegated to Get-SecurityDescriptor module)
        3. Access rule iteration and processing
        4. Identity resolution and orphaned SID testing (delegated to Resolve-SIDIdentity module)
        5. Result object creation (delegated to New-SIDResult module)
        6. Processing statistics compilation and memory cleanup
 
        PROCESSING STATISTICS:
        - Total objects processed and skipped
        - Access rules processed and orphaned SIDs found
        - Error counts and success rates
        - Processing timing and performance metrics
        - Memory usage and cleanup statistics
 
        ERROR HANDLING:
        - Graceful handling of error objects from pipeline
        - Individual access rule error containment
        - Comprehensive logging with correlation tracking
        - Processing statistics for troubleshooting
        - Memory cleanup for large datasets
 
        PERFORMANCE CHARACTERISTICS:
        - Optimized for pipeline processing of large object collections
        - Memory-efficient with periodic cleanup
        - Per-object processing isolation for error containment
        - Detailed performance metrics and timing
        - Scalable architecture for enterprise environments
 
        INTEGRATION POINTS:
        - Get-ObjectAccessRule for security descriptor processing
        - Test-AccessRuleForOrphanedSID for identity resolution
        - Correlation tracking across all processing modules
        - Enterprise logging and audit trail integration
    #>


    [CmdletBinding()]
    [OutputType([OrphanedSIDResult[]])]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [PSObject[]]$ADObject,

        [Parameter()]
        [switch]$IncludeInherited,

        [Parameter()]
        [string]$CurrentDomainSID,

        [Parameter()]
        [string]$CorrelationId = [System.Guid]::NewGuid().ToString()
    )

    process {
        foreach ($obj in $ADObject) {
            $results = [System.Collections.Generic.List[OrphanedSIDResult]]::new()

            # Initialize processing statistics for this object
            $processingStats = @{
                ObjectDN = "Unknown"
                StartTime = Get-Date
                ProcessedSIDs = 0
                SkippedSIDs = 0
                WellKnownSIDsSkipped = 0
                OrphanedSIDsFound = 0
                ErrorsEncountered = 0
                AccessRulesRetrieved = 0
                ProcessingMethod = "Orchestrated-Workflow"
            }

            try {
                Write-StructuredLog "Starting orchestrated orphaned SID detection for object" -Level Debug -CorrelationId $CorrelationId

                # Handle error objects that may come through the pipeline
                if ($obj.PSObject.Properties['IsError'] -and $obj.IsError -eq $true) {
                    Write-StructuredLog "Skipping error object: $($obj.ErrorMessage)" -Level Debug -CorrelationId $CorrelationId
                    $processingStats.ErrorsEncountered++
                    continue
                }

                # Validate that this is an AD object with required properties
                if (-not $obj.DistinguishedName) {
                    Write-StructuredLog "Object missing DistinguishedName property, skipping" -Level Debug -CorrelationId $CorrelationId
                    $processingStats.SkippedSIDs++
                    continue
                }

                $objectDN = $obj.DistinguishedName
                $processingStats.ObjectDN = $objectDN
                Write-StructuredLog "Processing object: $objectDN" -Level Debug -CorrelationId $CorrelationId

                # Check for security descriptor
                if (-not $obj.nTSecurityDescriptor) {
                    Write-StructuredLog "No security descriptor found for $objectDN" -Level Debug -CorrelationId $CorrelationId
                    $processingStats.SkippedSIDs++
                    continue
                }

                # Delegate security descriptor processing to specialized module
                $accessRules = Get-ObjectAccessRule -ADObject $obj -IncludeInherited:$IncludeInherited -CorrelationId $CorrelationId

                if (-not $accessRules) {
                    Write-StructuredLog "No access rules retrieved for $objectDN" -Level Debug -CorrelationId $CorrelationId
                    $processingStats.SkippedSIDs++
                    continue
                }

                $processingStats.AccessRulesRetrieved = $accessRules.Count
                Write-StructuredLog "Retrieved $($accessRules.Count) access rules for processing" -Level Debug -CorrelationId $CorrelationId

                # Process each access rule for orphaned SIDs using specialized identity resolution
                foreach ($rule in $accessRules) {
                    try {
                        # Delegate identity resolution and orphaned SID testing to specialized module
                        $orphanedResult = Test-AccessRuleForOrphanedSID -AccessRule $rule -ObjectDN $objectDN -ObjectClass $obj.ObjectClass -CurrentDomainSID $CurrentDomainSID -CorrelationId $CorrelationId

                        if ($orphanedResult) {
                            $results.Add($orphanedResult)
                            $processingStats.OrphanedSIDsFound++
                            Write-StructuredLog "Orphaned SID detected and result object created for $objectDN" -Level Verbose -CorrelationId $CorrelationId
                        }

                        $processingStats.ProcessedSIDs++
                    }
                    catch {
                        Write-StructuredLog "Error processing access rule for $objectDN : $($_.Exception.Message)" -Level Warning -CorrelationId $CorrelationId
                        $processingStats.SkippedSIDs++
                        $processingStats.ErrorsEncountered++
                    }
                }

                # Calculate processing duration and log statistics
                $processingStats.EndTime = Get-Date
                $processingStats.Duration = ($processingStats.EndTime - $processingStats.StartTime).TotalMilliseconds

                Write-StructuredLog "Completed processing $objectDN - Processed: $($processingStats.ProcessedSIDs), Orphaned found: $($processingStats.OrphanedSIDsFound), Duration: $($processingStats.Duration)ms" -Level Debug -CorrelationId $CorrelationId

                # Add processing statistics to results if any orphaned SIDs were found
                if ($results.Count -gt 0) {
                    foreach ($result in $results) {
                        if (-not $result.PSObject.Properties['ProcessingStatistics']) {
                            $result | Add-Member -NotePropertyName 'ProcessingStatistics' -NotePropertyValue $processingStats
                        }
                    }
                }

                # Clean up access rules reference to help with memory management
                $accessRules = $null

                # Periodic garbage collection for large processing runs
                if ($processingStats.ProcessedSIDs -gt 0 -and $processingStats.ProcessedSIDs % 100 -eq 0) {
                    Write-StructuredLog "Performing periodic garbage collection after processing $($processingStats.ProcessedSIDs) SIDs" -Level Debug -CorrelationId $CorrelationId
                    [System.GC]::Collect()
                    [System.GC]::WaitForPendingFinalizers()
                }

                # Emit this object's results and carry on: a return here would end the foreach and skip
                # every object after the first when several arrive by parameter
                $results.ToArray()
            }
            catch {
                $processingStats.ErrorsEncountered++
                $processingStats.EndTime = Get-Date
                Write-StructuredLog "Error in orchestrated processing for object $($processingStats.ObjectDN) : $($_.Exception.Message)" -Level Error -CorrelationId $CorrelationId

                # Log final processing statistics even on error
                Write-StructuredLog "Processing statistics for $($processingStats.ObjectDN) : Processed: $($processingStats.ProcessedSIDs), Errors: $($processingStats.ErrorsEncountered), Orphaned found: $($processingStats.OrphanedSIDsFound)" -Level Debug -CorrelationId $CorrelationId

                $results.ToArray()
            }
        }
    }
}

function Get-SIDProcessingStatistics {
    <#
    .SYNOPSIS
        Generates comprehensive processing statistics from orphaned SID results
 
    .DESCRIPTION
        Analyzes collections of OrphanedSIDResult objects to generate detailed
        processing statistics and performance metrics for reporting and optimization.
 
    .PARAMETER OrphanedSIDResults
        Collection of OrphanedSIDResult objects to analyze
 
    .PARAMETER CorrelationId
        Unique identifier for tracking this operation
 
    .OUTPUTS
        [PSCustomObject] Comprehensive processing statistics and performance metrics
 
    .NOTES
        STATISTICS GENERATED:
        - Processing counts and success rates
        - Performance timing and throughput metrics
        - Error analysis and categorization
        - Memory usage and efficiency metrics
        - Module performance breakdown
    #>


    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '',
        Justification = 'Statistics is a mass noun for the aggregate counters')]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [OrphanedSIDResult[]]$OrphanedSIDResults,

        [Parameter()]
        [string]$CorrelationId = [System.Guid]::NewGuid().ToString()
    )

    process {
        try {
            Write-StructuredLog "Generating processing statistics for $($OrphanedSIDResults.Count) results" -Level Debug -CorrelationId $CorrelationId

            # Extract processing statistics from results
            # @(...): each statistics entry is a hashtable, so for one result .Count would be its key count
            $allStats = @($OrphanedSIDResults | Where-Object { $_.PSObject.Properties['ProcessingStatistics'] } | ForEach-Object { $_.ProcessingStatistics })

            if ($allStats.Count -eq 0) {
                Write-StructuredLog "No processing statistics found in results" -Level Warning -CorrelationId $CorrelationId
                return $null
            }

            # Calculate aggregate statistics
            $statistics = [PSCustomObject]@{
                # Basic counts
                TotalObjectsProcessed = $allStats.Count
                TotalSIDsProcessed = ($allStats | Measure-Object -Property ProcessedSIDs -Sum).Sum
                TotalOrphanedSIDsFound = ($allStats | Measure-Object -Property OrphanedSIDsFound -Sum).Sum
                TotalErrorsEncountered = ($allStats | Measure-Object -Property ErrorsEncountered -Sum).Sum
                TotalSIDsSkipped = ($allStats | Measure-Object -Property SkippedSIDs -Sum).Sum

                # Performance metrics
                AverageProcessingTimeMS = ($allStats | Measure-Object -Property Duration -Average).Average
                TotalProcessingTimeMS = ($allStats | Measure-Object -Property Duration -Sum).Sum
                MaxProcessingTimeMS = ($allStats | Measure-Object -Property Duration -Maximum).Maximum
                MinProcessingTimeMS = ($allStats | Measure-Object -Property Duration -Minimum).Minimum

                # Success rates
                SuccessRate = if ($allStats.Count -gt 0) {
                    [Math]::Round((($allStats.Count - ($allStats | Measure-Object -Property ErrorsEncountered -Sum).Sum) / $allStats.Count) * 100, 2)
                } else { 0 }

                OrphanedSIDDetectionRate = if (($allStats | Measure-Object -Property ProcessedSIDs -Sum).Sum -gt 0) {
                    [Math]::Round((($allStats | Measure-Object -Property OrphanedSIDsFound -Sum).Sum) / (($allStats | Measure-Object -Property ProcessedSIDs -Sum).Sum) * 100, 2)
                } else { 0 }

                # Processing method analysis
                ProcessingMethods = $OrphanedSIDResults | Group-Object -Property ProcessingMethod | ForEach-Object {
                    [PSCustomObject]@{
                        Method = $_.Name
                        Count = $_.Count
                        Percentage = [Math]::Round(($_.Count / $OrphanedSIDResults.Count) * 100, 2)
                    }
                }

                # Performance per object analysis
                PerformanceAnalysis = @{
                    AverageSIDsPerObject = if ($allStats.Count -gt 0) {
                        [Math]::Round((($allStats | Measure-Object -Property ProcessedSIDs -Sum).Sum) / $allStats.Count, 2)
                    } else { 0 }

                    AverageOrphanedSIDsPerObject = if ($allStats.Count -gt 0) {
                        [Math]::Round((($allStats | Measure-Object -Property OrphanedSIDsFound -Sum).Sum) / $allStats.Count, 2)
                    } else { 0 }

                    ProcessingThroughput = if (($allStats | Measure-Object -Property Duration -Sum).Sum -gt 0) {
                        [Math]::Round((($allStats | Measure-Object -Property ProcessedSIDs -Sum).Sum) / (($allStats | Measure-Object -Property Duration -Sum).Sum / 1000), 2)
                    } else { 0 }
                }

                # Timing analysis
                TimingAnalysis = @{
                    EarliestProcessing = ($allStats | Where-Object StartTime | Measure-Object -Property StartTime -Minimum).Minimum
                    LatestProcessing = ($allStats | Where-Object EndTime | Measure-Object -Property EndTime -Maximum).Maximum
                    TotalDurationSeconds = if (($allStats | Measure-Object -Property Duration -Sum).Sum -gt 0) {
                        [Math]::Round((($allStats | Measure-Object -Property Duration -Sum).Sum) / 1000, 2)
                    } else { 0 }
                }

                # Error analysis
                ErrorAnalysis = @{
                    ObjectsWithErrors = ($allStats | Where-Object { $_.ErrorsEncountered -gt 0 }).Count
                    ErrorRate = if ($allStats.Count -gt 0) {
                        [Math]::Round((($allStats | Where-Object { $_.ErrorsEncountered -gt 0 }).Count / $allStats.Count) * 100, 2)
                    } else { 0 }
                    AverageErrorsPerFailedObject = if (($allStats | Where-Object { $_.ErrorsEncountered -gt 0 }).Count -gt 0) {
                        [Math]::Round((($allStats | Measure-Object -Property ErrorsEncountered -Sum).Sum) / (($allStats | Where-Object { $_.ErrorsEncountered -gt 0 }).Count), 2)
                    } else { 0 }
                }

                # Metadata
                GeneratedAt = Get-Date
                CorrelationId = $CorrelationId
                GeneratedBy = $env:USERNAME
                MachineName = $env:COMPUTERNAME
            }

            Write-StructuredLog "Generated comprehensive processing statistics: $($statistics.TotalObjectsProcessed) objects, $($statistics.TotalSIDsProcessed) SIDs, $($statistics.TotalOrphanedSIDsFound) orphaned" -Level Debug -CorrelationId $CorrelationId

            return $statistics
        }
        catch {
            Write-StructuredLog "Error generating processing statistics: $($_.Exception.Message)" -Level Error -CorrelationId $CorrelationId
            throw
        }
    }
}

#endregion