Private/Core/Invoke-MainProcessingLogic.ps1

#Requires -Version 5.1

<#
.SYNOPSIS
    Core SID discovery and processing logic for Find-UnknownSID
 
.DESCRIPTION
    Handles the main processing workflow including AD object discovery,
    SID analysis, and optional removal operations. Focused solely on
    the core business logic of orphaned SID processing.
 
.NOTES
    Author: Jeffrey Stuhr
    Blog: https://www.techbyjeff.net
    LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/
    Last Updated: 2025-07-04
    Version: 2.0.0
 
    TROUBLESHOOTING:
    - For processing failures: .\Troubleshooting\Common\Processing-Issues.md
    - For AD connectivity: .\Troubleshooting\Common\AD-Connection-Issues.md
#>



function Invoke-MainProcessingLogic {
    <#
    .SYNOPSIS
        Execute the main SID discovery and processing logic
 
    .DESCRIPTION
        Orchestrates the main processing workflow including AD object discovery,
        SID analysis, and optional removal operations with comprehensive error
        handling and performance monitoring.
 
        BUSINESS VALUE:
        - Identifies orphaned SIDs that pose security risks
        - Enables cleanup of obsolete access control entries
        - Provides comprehensive audit trail for compliance
        - Supports large-scale AD security remediation
        - Delivers actionable security intelligence
 
        PROCESSING FEATURES:
        - Parallel processing for improved performance
        - Memory-efficient streaming results management
        - Comprehensive retry logic for resilient operations
        - Real-time progress reporting and monitoring
        - Integration with backup systems for safe removal
 
    .PARAMETER SearchBase
        [String[]] (Mandatory) Distinguished Names of search bases for processing.
        Each DN must be valid and accessible in Active Directory.
 
        VALIDATION RULES:
        - Must pass Test-ValidDistinguishedName validation
        - Each DN must exist in Active Directory
        - User must have read permissions to the specified containers
 
        BUSINESS CONTEXT:
        Defines the scope of orphaned SID discovery operation.
        Consider organizational structure when selecting search bases.
 
    .PARAMETER Remove
        [Switch] (Optional) Whether to perform SID removal operations.
        Requires additional permissions and triggers backup creation.
 
        SECURITY IMPLICATIONS:
        - Requires elevated privileges for ACL modification
        - Should be tested thoroughly in non-production environment
        - Backup creation is strongly recommended
 
    .PARAMETER IncludeInherited
        [Switch] (Optional) Whether to include inherited ACEs in processing.
        Affects scope and performance of SID analysis.
 
        PERFORMANCE IMPACT:
        Including inherited ACEs significantly increases processing time
        but provides more comprehensive security analysis.
 
    .PARAMETER MaxRetries
        [Int] (Optional) Maximum retry attempts for failed operations (Default: 3).
        Provides resilience against transient failures.
 
    .PARAMETER BackupPath
        [String] (Optional) Directory path for storing ACL backups.
        Required for removal operations in production environments.
 
    .PARAMETER CorrelationId
        [String] (Optional) Correlation ID for tracking this operation.
        Auto-generated if not provided for audit purposes.
 
    .OUTPUTS
        [Hashtable] Processing results containing:
        - TotalObjectsProcessed: Number of AD objects analyzed
        - OrphanedSIDsFound: Count of orphaned SIDs discovered
        - ProcessingErrors: Number of errors encountered
        - ProcessingDuration: Total execution time
        - ObjectsPerSecond: Processing performance metric
        - PeakMemoryUsageMB: Maximum memory consumption
        - StreamingSummary: Results summary from streaming manager
        - RemovalResults: Details of removal operations (if performed)
        - StreamingManager: Reference for accessing detailed results
 
    .EXAMPLE
        PS> Invoke-MainProcessingLogic -SearchBase "OU=Users,DC=contoso,DC=com"
 
        DESCRIPTION: Execute discovery mode on Users OU
        OUTPUT: Hashtable with discovered orphaned SIDs and statistics
        USE CASE: Standard orphaned SID discovery operation
        DURATION: Varies based on object count (typically 1-5 seconds per 100 objects)
 
    .EXAMPLE
        PS> Invoke-MainProcessingLogic -SearchBase @("OU=Users,DC=contoso,DC=com") -Remove -BackupPath "C:\Backups"
 
        DESCRIPTION: Execute removal operation with backup creation
        OUTPUT: Results including removal statistics and backup location
        BUSINESS CASE: Production cleanup of orphaned SIDs with safety measures
        COMPLIANCE: Supports change management and audit requirements
 
    .EXAMPLE
        PS> Invoke-MainProcessingLogic -SearchBase "DC=contoso,DC=com" -IncludeInherited
 
        DESCRIPTION: Comprehensive domain-wide analysis with inherited permissions
        OUTPUT: Complete security analysis results for entire domain
        ENTERPRISE USE: Large-scale security assessment and remediation
        PERFORMANCE: Optimized for reliable sequential processing
 
    .NOTES
        Author: Jeffrey Stuhr
        Blog: https://www.techbyjeff.net
        LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/
 
        SECURITY CONSIDERATIONS:
        - Removal operations require elevated AD permissions
        - Backup creation is essential for production safety
        - All operations are logged for security audit
        - ShouldProcess support enables safe testing with -WhatIf
 
        PERFORMANCE CHARACTERISTICS:
        - Processing rate: 50-200 objects per second (depending on complexity)
        - Memory usage: Optimized with streaming results management
        - Parallel processing scales with available system resources
        - Progress reporting every 50 objects for user feedback
 
        TROUBLESHOOTING:
        - For processing failures: .\Troubleshooting\Common\Processing-Issues.md
        - For AD connectivity: .\Troubleshooting\Common\AD-Connection-Issues.md
        - For memory issues: .\Troubleshooting\Performance\Memory-Management.md
        - For permission problems: .\Troubleshooting\Security\Permission-Issues.md
 
        KNOWN LIMITATIONS:
        - Large datasets may require memory management tuning
        - Network latency affects processing speed
        - Some ACL operations may require domain administrator privileges
        - Streaming results are temporarily stored on disk
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([hashtable])]
    param(
        [Parameter(Mandatory)]
        [ValidateScript({ Test-ValidDistinguishedName $_ })]
        [string[]]$SearchBase,

        [Parameter()]
        [switch]$Remove,

        [Parameter()]
        [switch]$IncludeInherited,

        [Parameter()]
        [ValidateRange(1, 10)]
        [int]$MaxRetries = 3,

        [Parameter()]
        [string]$BackupPath,

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

    begin {
        Write-StructuredLog "Starting main processing logic" -Level Information -Component 'MainProcessing' -CorrelationId $CorrelationId

        # StreamingResultsManager class is loaded via SecureClassImporter in main script
        # Verify it's available before proceeding
        if (-not ([System.Management.Automation.PSTypeName]'StreamingResultsManager').Type) {
            throw "StreamingResultsManager class not available - ensure SecureClassImporter loaded all classes successfully"
        }        # Initialize streaming results manager instead of in-memory arrays
        $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "OrphanedSid-$CorrelationId"

        # -WhatIf on the public command arrives here as $WhatIfPreference. Read it directly: asking
        # ShouldProcess would print a "What if" line about the temp directory, which is not what the
        # caller is rehearsing.
        $whatIfMode = [bool]$WhatIfPreference

        # Batch size comes from the BatchSize configuration key (default 50)
        $batchSize = 50
        if ($script:Config -and $script:Config.BatchSize -ge 1) {
            $batchSize = [int]$script:Config.BatchSize
        }

        # The domain SID lets Get-SIDAnalysis tell a SID from this domain apart from a foreign or
        # unknown one. Discovery still works without it; every domain SID is then rated Unknown Domain.
        $script:CurrentDomainSID = $null
        try {
            $script:CurrentDomainSID = (Get-ADDomain -ErrorAction Stop).DomainSID.Value
        }
        catch {
            Write-StructuredLog "Could not determine the current domain SID: $($_.Exception.Message)" -Level Warning -Component 'MainProcessing' -CorrelationId $CorrelationId
        }

        # Create StreamingResultsManager with 2-argument constructor (doesn't create directory yet)
        $script:StreamingResults = [StreamingResultsManager]::new($tempDir, $batchSize)

        # Configure WhatIf mode and complete initialization (this will handle directory creation properly)
        $script:StreamingResults.ConfigureWhatIfMode($whatIfMode)

        $script:RemovalResults = @()

        Write-StructuredLog "Initialized streaming results manager with temp directory: $tempDir (WhatIf: $whatIfMode)" -Level Debug -Component 'MainProcessing' -CorrelationId $CorrelationId
    }

    process {
        try {
            # Process each search base
            foreach ($searchPath in $SearchBase) {
                Write-StructuredLog "Processing search base: $searchPath" -Level Information -Component 'MainProcessing' -CorrelationId $CorrelationId

                # Log security audit for AD discovery operation attempt
                Write-ADOperationSecurityLog -OperationName 'AD-ObjectDiscovery' -Outcome 'Attempt' -SecurityContext @{
                    SearchBase = $searchPath
                    Operation = 'Get-ADObjectsSequential'
                    Mode = if ($Remove) { 'RemovalMode' } else { 'DiscoveryMode' }
                } -CorrelationId $CorrelationId

                # Always perform discovery phase - ShouldProcess only applies to removal operations
                # Get AD objects for processing using refactored sequential processing
                try {
                    $adObjects = Get-ADObjectsSequential -SearchBase @($searchPath)

                    # Log successful AD discovery
                    Write-ADOperationSecurityLog -OperationName 'AD-ObjectDiscovery' -Outcome 'Success' -SecurityContext @{
                        SearchBase = $searchPath
                        ObjectsFound = @($adObjects).Count
                        Operation = 'Get-ADObjectsSequential'
                    } -CorrelationId $CorrelationId
                } catch {
                    # Log failed AD discovery
                    Write-ADOperationSecurityLog -OperationName 'AD-ObjectDiscovery' -Outcome 'Failure' -SecurityContext @{
                        SearchBase = $searchPath
                        ErrorMessage = $_.Exception.Message
                        Operation = 'Get-ADObjectsSequential'
                    } -CorrelationId $CorrelationId
                    throw
                }

                # Ensure $adObjects is always treated as an array for consistent counting
                $objectArray = @($adObjects)

                # Categorize objects for meaningful user feedback
                $objectStats = @{
                    Users = 0
                    Computers = 0
                    Groups = 0
                    OUs = 0
                    Other = 0
                }

                foreach ($obj in $objectArray) {
                    switch ($obj.ObjectClass) {
                        'user' { $objectStats.Users++ }
                        'computer' { $objectStats.Computers++ }
                        'group' { $objectStats.Groups++ }
                        'organizationalUnit' { $objectStats.OUs++ }
                        default { $objectStats.Other++ }
                    }
                }

                # Report meaningful discovery summary
                $summaryParts = @()
                if ($objectStats.Users -gt 0) { $summaryParts += "$($objectStats.Users) users" }
                if ($objectStats.Computers -gt 0) { $summaryParts += "$($objectStats.Computers) computers" }
                if ($objectStats.Groups -gt 0) { $summaryParts += "$($objectStats.Groups) groups" }
                if ($objectStats.OUs -gt 0) { $summaryParts += "$($objectStats.OUs) OUs" }
                if ($objectStats.Other -gt 0) { $summaryParts += "$($objectStats.Other) other objects" }

                $summaryText = if ($summaryParts.Count -gt 0) { $summaryParts -join ', ' } else { "0 objects" }
                Write-StructuredLog "Discovered: $summaryText (Total: $($objectArray.Count) objects)" -Level Information -Component 'MainProcessing' -CorrelationId $CorrelationId

                Write-StructuredLog "Found $($objectArray.Count) AD objects to process" -Level Debug -Component 'MainProcessing' -CorrelationId $CorrelationId
                $script:Statistics.TotalObjects += $objectArray.Count

                # Process objects for orphaned SIDs - this always happens regardless of WhatIf
                $processedCount = 0
                $orphanedFoundCount = 0
                $null = $objectArray | ForEach-Object {
                    try {
                        # Pinned: housekeeping, not the operation -WhatIf rehearses
                        Invoke-MemoryCheck -MemoryManager $script:MemoryManager -WhatIf:$false
                        $script:Statistics.ProcessedObjects++
                        $processedCount++

                        # Show progress every 50 objects or on last object
                        if (($processedCount % 50 -eq 0) -or ($processedCount -eq $objectArray.Count)) {
                            $percentComplete = [Math]::Round(($processedCount / $objectArray.Count) * 100, 1)
                            Write-StructuredLog "Processing: $processedCount/$($objectArray.Count) objects ($percentComplete%) - Orphaned SIDs found: $orphanedFoundCount" -Level Information -Component 'MainProcessing' -CorrelationId $CorrelationId
                        }

                        $orphanedResults = Find-OrphanedSIDsInObject -ADObject $_ -IncludeInherited:$IncludeInherited -CurrentDomainSID $script:CurrentDomainSID

                        # Log security audit for orphaned SID analysis
                        if ($orphanedResults -and $orphanedResults.Count -gt 0) {
                            Write-ADOperationSecurityLog -OperationName 'OrphanedSID-Detection' -Outcome 'Success' -SecurityContext @{
                                ObjectDN = $_.DistinguishedName
                                ObjectClass = $_.ObjectClass
                                OrphanedSIDsFound = $orphanedResults.Count
                                OrphanedSIDs = ($orphanedResults | ForEach-Object { $_.OrphanedSID }) -join ','
                            } -CorrelationId $CorrelationId
                        }

                        # Additional memory management for large operations
                        if ($processedCount % 100 -eq 0) {
                            # Use centralized cleanup for large operations
                            # Pinned: housekeeping, not the operation -WhatIf rehearses
                            $cleanupResult = Invoke-Cleanup -CorrelationId $CorrelationId -WhatIf:$false
                            Write-Verbose "Memory usage at $processedCount objects: $($cleanupResult.MemoryAfterMB) MB (Freed: $($cleanupResult.MemoryFreedMB) MB)"
                        }

                        if ($orphanedResults -and $orphanedResults.Count -gt 0) {
                            $script:Statistics.OrphanedSIDsFound += $orphanedResults.Count
                            $orphanedFoundCount += $orphanedResults.Count

                            # Stream results to disk instead of accumulating in memory
                            foreach ($orphanedResult in $orphanedResults) {
                                $script:StreamingResults.AddResult($orphanedResult)
                            }

                            if ($Remove) {
                                # Apply ShouldProcess only to actual removal operations
                                foreach ($orphanedResult in $orphanedResults) {
                                    $target = "$($orphanedResult.ObjectDN) - SID: $($orphanedResult.OrphanedSID)"
                                    if ($PSCmdlet.ShouldProcess($target, "Remove Orphaned SID")) {
                                        Write-StructuredLog "Removing orphaned SID: $($orphanedResult.OrphanedSID) from $($orphanedResult.ObjectDN)" -Level Verbose -Component 'MainProcessing' -CorrelationId $CorrelationId

                                        # Log security audit for SID removal attempt
                                        Write-ADOperationSecurityLog -OperationName 'OrphanedSID-Removal' -Outcome 'Attempt' -SecurityContext @{
                                            ObjectDN = $orphanedResult.ObjectDN
                                            OrphanedSID = $orphanedResult.OrphanedSID
                                            BackupPath = $BackupPath
                                            Operation = 'Remove-OrphanedSidEntry'
                                        } -CorrelationId $CorrelationId

                                        $removalParams = @{
                                            ObjectDN = $orphanedResult.ObjectDN
                                            OrphanedSIDs = @($orphanedResult.OrphanedSID)
                                            CorrelationId = $CorrelationId
                                        }
                                        if ($BackupPath) {
                                            $removalParams.BackupPath = $BackupPath
                                        }

                                        try {
                                            $removalResult = Remove-OrphanedSidEntry @removalParams
                                            $script:RemovalResults += $removalResult

                                            # Log successful SID removal
                                            Write-ADOperationSecurityLog -OperationName 'OrphanedSID-Removal' -Outcome 'Success' -SecurityContext @{
                                                ObjectDN = $orphanedResult.ObjectDN
                                                OrphanedSID = $orphanedResult.OrphanedSID
                                                BackupCreated = $removalResult.BackupCreated
                                                BackupPath = $removalResult.BackupPath
                                                RemovedCount = $removalResult.RemovedCount
                                            } -CorrelationId $CorrelationId
                                        } catch {
                                            # Log failed SID removal
                                            Write-ADOperationSecurityLog -OperationName 'OrphanedSID-Removal' -Outcome 'Failure' -SecurityContext @{
                                                ObjectDN = $orphanedResult.ObjectDN
                                                OrphanedSID = $orphanedResult.OrphanedSID
                                                ErrorMessage = $_.Exception.Message
                                            } -CorrelationId $CorrelationId
                                            throw
                                        }
                                    } else {
                                        Write-StructuredLog "WhatIf: Would remove orphaned SID: $($orphanedResult.OrphanedSID) from $($orphanedResult.ObjectDN)" -Level Verbose -Component 'MainProcessing' -CorrelationId $CorrelationId

                                        # Log WhatIf simulation for audit trail
                                        Write-ADOperationSecurityLog -OperationName 'OrphanedSID-Removal' -Outcome 'Attempt' -SecurityContext @{
                                            ObjectDN = $orphanedResult.ObjectDN
                                            OrphanedSID = $orphanedResult.OrphanedSID
                                            SimulationMode = $true
                                            WhatIfMode = $true
                                        } -CorrelationId $CorrelationId
                                        # Create a mock removal result for WhatIf mode
                                        $mockResult = [PSCustomObject]@{
                                            ObjectDN = $orphanedResult.ObjectDN
                                            OrphanedSID = $orphanedResult.OrphanedSID
                                            Success = $true
                                            ProcessingTime = [TimeSpan]::Zero
                                            ErrorMessage = $null
                                            RemovedSIDs = @($orphanedResult.OrphanedSID)
                                            FailedSIDs = @()
                                            BlockedSIDs = @()
                                            SecurityValidation = 'WhatIf - Would Remove'
                                        }
                                        $script:RemovalResults += $mockResult
                                    }
                                }
                            }

                            return $orphanedResults
                        }
                    }
                    catch {
                        $script:Statistics.ProcessingErrors++
                        Write-StructuredLog "Error processing object $($_.DistinguishedName): $($_.Exception.Message)" -Level Error -Component 'MainProcessing' -CorrelationId $CorrelationId

                        # Implement retry logic using MaxRetries parameter
                        $retryCount = 0
                        $retrySuccessful = $false

                        while ($retryCount -lt $MaxRetries -and -not $retrySuccessful) {
                            $retryCount++
                            Write-StructuredLog "Retry attempt $retryCount of $MaxRetries for object $($_.DistinguishedName)" -Level Warning -Component 'MainProcessing' -CorrelationId $CorrelationId

                            try {
                                Start-Sleep -Seconds (2 * $retryCount)  # Exponential backoff
                                $orphanedResults = Find-OrphanedSIDsInObject -ADObject $_ -IncludeInherited:$IncludeInherited -CurrentDomainSID $script:CurrentDomainSID
                                $retrySuccessful = $true

                                if ($orphanedResults -and $orphanedResults.Count -gt 0) {
                                    $script:Statistics.OrphanedSIDsFound += $orphanedResults.Count
                                    # Stream retry results to disk as well
                                    foreach ($orphanedResult in $orphanedResults) {
                                        $script:StreamingResults.AddResult($orphanedResult)
                                    }
                                    return $orphanedResults
                                }
                            }
                            catch {
                                Write-StructuredLog "Retry $retryCount failed for object $($_.DistinguishedName): $($_.Exception.Message)" -Level Warning -Component 'MainProcessing' -CorrelationId $CorrelationId
                                if ($retryCount -eq $MaxRetries) {
                                    Write-StructuredLog "Max retries ($MaxRetries) exceeded for object $($_.DistinguishedName)" -Level Error -Component 'MainProcessing' -CorrelationId $CorrelationId
                                }
                            }
                        }
                    }
                }

                # Show completion summary for this search base
                Write-StructuredLog "Completed $searchPath - Objects: $($objectArray.Count), Orphaned SIDs: $orphanedFoundCount" -Level Information -Component 'MainProcessing' -CorrelationId $CorrelationId
            }

            # Complete statistics
            $script:Statistics.Complete()

            # Get summary from streaming results manager
            $streamingSummary = $script:StreamingResults.GetSummary()

            # Generate processing summary with streaming results
            $processingResults = @{
                TotalObjectsProcessed = $script:Statistics.ProcessedObjects
                OrphanedSIDsFound = $script:Statistics.OrphanedSIDsFound
                ProcessingErrors = $script:Statistics.ProcessingErrors
                ProcessingDuration = $script:Statistics.Duration
                ObjectsPerSecond = $script:Statistics.ObjectsPerSecond
                PeakMemoryUsageMB = $script:MemoryManager.GetPeakMemoryUsage()
                StreamingSummary = $streamingSummary
                RemovalResults = $script:RemovalResults
                CorrelationId = $CorrelationId
                # Store reference to streaming manager for later access
                StreamingManager = $script:StreamingResults
            }

            Write-StructuredLog "Main processing completed successfully" -Level Information -Component 'MainProcessing' -CorrelationId $CorrelationId
            return $processingResults

        }
        catch {
            $script:Statistics.CriticalErrors++
            Write-StructuredLog "Critical error in main processing: $($_.Exception.Message)" -Level Error -Component 'MainProcessing' -CorrelationId $CorrelationId
            throw
        }
    }
}