Private/Backup/Restore-ACLOperation.ps1

#region Restore-ACLOperation.ps1 - Core ACL Restoration Module
<#
.SYNOPSIS
    Core ACL restoration and manipulation functions for restore operations.
 
.DESCRIPTION
    This module provides focused ACL restoration functionality including:
    - Direct ACL application to Active Directory objects
    - Target object verification and validation
    - Restoration success confirmation and verification
    - SDDL conversion and security descriptor manipulation
 
    This module handles the core security operations for ACL restoration,
    working with validated backup data to apply permissions safely.
 
.NOTES
    Author: Jeffrey Stuhr
    Module: Find-UnknownSID.RestoreOperations
    Dependencies: ActiveDirectory module, validated backup data
    Version: 2.1.0
 
    SECURITY CONSIDERATIONS:
    - Requires appropriate Active Directory permissions
    - Validates target object existence before modification
    - Confirms restoration success through verification
    - Logs all ACL modification operations for audit compliance
#>


function Set-ObjectACL {
    <#
    .SYNOPSIS
        Applies ACL settings to Active Directory objects from validated backup data.
 
    .DESCRIPTION
        Core function for applying Access Control Lists to Active Directory objects
        using validated backup data. Handles proper security descriptor conversion,
        application, and verification of successful ACL restoration.
 
        This function performs the actual ACL modification operations and should
        only be called with validated backup data from the Test-BackupValidation module.
 
        BUSINESS VALUE:
        - Enables precise ACL restoration for disaster recovery
        - Provides granular control over permission restoration
        - Ensures secure and validated ACL application
        - Supports compliance requirements for change management
 
    .PARAMETER TargetObjectDN
        Distinguished name of the target Active Directory object for ACL application.
        Object must exist and be accessible for ACL modification.
 
    .PARAMETER BackupData
        Validated backup data containing SDDL and metadata for restoration.
        Must be validated using Test-BackupIntegrity before use.
 
    .PARAMETER VerifyApplication
        Whether to verify successful ACL application after setting.
        Recommended for critical operations requiring confirmation.
 
    .PARAMETER WhatIf
        Shows what ACL changes would be applied without making modifications.
        Useful for validation and approval workflows.
 
    .PARAMETER CorrelationId
        Unique identifier for tracking this ACL operation across logs.
 
    .EXAMPLE
        PS> Set-ObjectACL -TargetObjectDN "CN=TestUser,CN=Users,DC=contoso,DC=com" -BackupData $validatedBackup
 
        DESCRIPTION: Applies ACL from validated backup to target user object
        OUTPUT: ACLOperationResult with success status and details
        USE CASE: Direct ACL restoration for specific object
 
    .EXAMPLE
        PS> Set-ObjectACL -TargetObjectDN $dn -BackupData $backup -VerifyApplication -WhatIf
 
        DESCRIPTION: Preview ACL application with verification enabled
        OUTPUT: Shows what would be applied and verification results
        USE CASE: Pre-validation for critical ACL changes
 
    .INPUTS
        [string] TargetObjectDN from pipeline
        [PSCustomObject] Validated backup data
 
    .OUTPUTS
        [PSCustomObject] ACLOperationResult with properties:
        - Success: Boolean indicating operation success
        - TargetObjectDN: DN of the modified object
        - ModificationsApplied: Number of ACL entries applied
        - VerificationResult: Optional verification outcome
        - Duration: Time taken for the operation
        - CorrelationId: Tracking identifier
 
    .NOTES
        Author: Jeffrey Stuhr
        Blog: https://www.techbyjeff.net
        LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/
 
        PERFORMANCE CHARACTERISTICS:
        - Average ACL application: 50-100ms per object
        - Verification adds 20-30ms overhead
        - Memory usage: ~1MB per concurrent operation
 
        SECURITY CONSIDERATIONS:
        - Requires Modify Permissions permission on target object
        - Validates SDDL format before application
        - Logs all ACL modifications for audit compliance
        - Confirms target object existence before modification
 
        TROUBLESHOOTING:
        - For permission errors: .\Troubleshooting\Security\Permission-Issues.md
        - For SDDL format issues: .\Troubleshooting\Common\Backup-Restore-Issues.md
        - For verification failures: .\Troubleshooting\Common\Backup-Restore-Issues.md
 
    .LINK
        .\Troubleshooting\Security\Permission-Issues.md
        .\Troubleshooting\Common\Backup-Restore-Issues.md
    #>


    [CmdletBinding(SupportsShouldProcess)]
    [OutputType('ACLOperationResult')]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$TargetObjectDN,

        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [PSCustomObject]$BackupData,

        [Parameter()]
        [switch]$VerifyApplication,

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

    begin {
        Write-Verbose "Starting ACL application operations - CorrelationId: $CorrelationId"

        # Validate backup data has required properties
        $requiredProperties = @('SDDL', 'ObjectDN', 'ValidationSignature')
        foreach ($prop in $requiredProperties) {
            if (-not $BackupData.PSObject.Properties[$prop]) {
                throw "Backup data missing required property: $prop"
            }
        }
    }

    process {
        $startTime = Get-Date

        try {
            Write-Verbose "Applying ACL to object: $TargetObjectDN - CorrelationId: $CorrelationId"

            # Validate target object exists and is accessible
            $targetValidation = Get-RestorationTarget -TargetObjectDN $TargetObjectDN -CorrelationId $CorrelationId
            if (-not $targetValidation.IsValid) {
                throw "Target object validation failed: $($targetValidation.ErrorMessage)"
            }

            # Prepare SDDL for application
            $sddlToApply = $BackupData.SDDL
            $modificationsCount = 0

            if ($PSCmdlet.ShouldProcess($TargetObjectDN, "Apply ACL from backup")) {
                try {
                    # Create security descriptor from SDDL
                    $securityDescriptor = [System.DirectoryServices.ActiveDirectorySecurity]::new()
                    $securityDescriptor.SetSecurityDescriptorSddlForm($sddlToApply)

                    # Apply ACL to Active Directory object
                    $adPath = "AD:$TargetObjectDN"
                    Set-Acl -Path $adPath -AclObject $securityDescriptor -ErrorAction Stop

                    # Count ACE entries for reporting
                    $modificationsCount = ($sddlToApply -split '\(' | Where-Object { $_ -like '*;*' }).Count

                    Write-Verbose "ACL applied successfully to $TargetObjectDN ($modificationsCount modifications) - CorrelationId: $CorrelationId"
                    $success = $true
                    $errorMessage = $null
                }
                catch {
                    $success = $false
                    # Sanitize error message to remove sensitive information
                    $sanitizedError = $_.Exception.Message -replace 'PASSWORD\d+', '[REDACTED]' -replace 'password\d+', '[REDACTED]'
                    $errorMessage = "ACL application failed: $sanitizedError"
                    Write-Error $errorMessage
                }
            }
            else {
                # WhatIf mode - simulate the operation
                $success = $true
                $errorMessage = $null
                $modificationsCount = ($sddlToApply -split '\(' | Where-Object { $_ -like '*;*' }).Count
                Write-Verbose "WhatIf: Would apply ACL with $modificationsCount modifications to $TargetObjectDN"
            }

            # Perform verification if requested and operation succeeded
            $verificationResult = $null
            if ($success -and $VerifyApplication -and -not $WhatIfPreference) {
                $verificationResult = Confirm-RestorationSuccess -TargetObjectDN $TargetObjectDN -ExpectedData $BackupData -CorrelationId $CorrelationId
            }

            $duration = (Get-Date) - $startTime

            $result = [PSCustomObject]@{
                PSTypeName = 'ACLOperationResult'
                Success = $success
                TargetObjectDN = $TargetObjectDN
                ModificationsApplied = $modificationsCount
                VerificationResult = $verificationResult
                Duration = $duration
                ErrorMessage = $errorMessage
                WhatIfMode = $WhatIfPreference
                CorrelationId = $CorrelationId
                CompletedAt = Get-Date
            }

            Write-Verbose "ACL operation completed - Success: $success - Duration: $($duration.TotalMilliseconds)ms - CorrelationId: $CorrelationId"
            return $result
        }
        catch {
            $duration = (Get-Date) - $startTime

            $errorResult = [PSCustomObject]@{
                PSTypeName = 'ACLOperationResult'
                Success = $false
                TargetObjectDN = $TargetObjectDN
                ModificationsApplied = 0
                VerificationResult = $null
                Duration = $duration
                ErrorMessage = "ACL operation failed: $($_.Exception.Message)"
                WhatIfMode = $WhatIfPreference
                CorrelationId = $CorrelationId
                CompletedAt = Get-Date
            }

            Write-Error "ACL operation failed for $TargetObjectDN : $($_.Exception.Message) - CorrelationId: $CorrelationId"
            return $errorResult
        }
    }

    end {
        Write-Verbose "Completed ACL application operations - CorrelationId: $CorrelationId"
    }
}


function Get-RestorationTarget {
    <#
    .SYNOPSIS
        Validates and prepares target object for ACL restoration.
 
    .DESCRIPTION
        Performs comprehensive validation of target Active Directory objects
        before ACL restoration operations. Ensures objects exist, are accessible,
        and are suitable for ACL modification.
 
    .PARAMETER TargetObjectDN
        Distinguished name of the target object to validate.
 
    .PARAMETER CheckPermissions
        Whether to verify current user has permissions to modify ACL.
 
    .PARAMETER CorrelationId
        Unique identifier for tracking this validation operation.
 
    .EXAMPLE
        PS> Get-RestorationTarget -TargetObjectDN "CN=TestUser,CN=Users,DC=contoso,DC=com"
 
        Validates target object for restoration readiness.
 
    .OUTPUTS
        [PSCustomObject] TargetValidationResult
    #>


    [CmdletBinding()]
    [OutputType('TargetValidationResult')]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [string]$TargetObjectDN,

        [Parameter()]
        [switch]$CheckPermissions,

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

    process {
        try {
            $issues = @()

            # Validate DN format (strict validation for security)
            if (-not ($TargetObjectDN -match '^(CN|OU|DC)=.+')) {
                $issues += "Invalid DN format: $TargetObjectDN"
            }

            # Check for potentially malicious input patterns
            $maliciousPatterns = @(
                '\.\.\/',           # Directory traversal
                '[A-Za-z]:\\',      # Windows paths
                '<script',          # Script injection
                'DROP\s+TABLE',     # SQL injection
                'javascript:'       # JavaScript protocol
            )
            
            foreach ($pattern in $maliciousPatterns) {
                if ($TargetObjectDN -match $pattern) {
                    $issues += "Invalid DN format: $TargetObjectDN"
                    break
                }
            }

            # Check if object exists
            $objectExists = $false
            $objectType = $null
            try {
                $adObject = Get-ADObject -Identity $TargetObjectDN -Properties objectClass -ErrorAction Stop
                $objectExists = $true
                $objectType = $adObject.objectClass[-1]  # Get most specific class
                Write-Verbose "Target object exists: $TargetObjectDN (Type: $objectType)"
            }
            catch {
                $issues += "Target object not found or inaccessible: $($_.Exception.Message)"
            }

            # Check ACL read permissions if object exists
            $aclReadable = $null
            if ($objectExists) {
                try {
                    $null = Get-Acl -Path "AD:$TargetObjectDN" -ErrorAction Stop
                    $aclReadable = $true
                    Write-Verbose "ACL is readable for target object: $TargetObjectDN"
                }
                catch {
                    $aclReadable = $false
                    $issues += "Cannot read current ACL: $($_.Exception.Message)"
                }
            }

            # Check write permissions if requested
            $writePermissions = $null
            if ($CheckPermissions -and $objectExists) {
                try {
                    # Try to read the security descriptor to check permissions
                    $testAccess = [System.DirectoryServices.DirectoryEntry]::new("LDAP://$TargetObjectDN")
                    $testAccess.RefreshCache(@("nTSecurityDescriptor"))
                    $writePermissions = $true
                    $testAccess.Dispose()
                }
                catch {
                    $writePermissions = $false
                    $issues += "Insufficient permissions to modify ACL: $($_.Exception.Message)"
                }
            }

            return [PSCustomObject]@{
                PSTypeName = 'TargetValidationResult'
                IsValid = $issues.Count -eq 0
                TargetObjectDN = $TargetObjectDN
                ObjectExists = $objectExists
                ObjectType = $objectType
                ACLReadable = $aclReadable
                WritePermissions = $writePermissions
                Issues = $issues
                CorrelationId = $CorrelationId
                ValidatedAt = Get-Date
            }
        }
        catch {
            return [PSCustomObject]@{
                PSTypeName = 'TargetValidationResult'
                IsValid = $false
                TargetObjectDN = $TargetObjectDN
                ObjectExists = $false
                ObjectType = $null
                ACLReadable = $false
                WritePermissions = $null
                Issues = @("Validation error: $($_.Exception.Message)")
                CorrelationId = $CorrelationId
                ValidatedAt = Get-Date
            }
        }
    }
}


function Confirm-RestorationSuccess {
    <#
    .SYNOPSIS
        Verifies successful ACL restoration by comparing applied vs expected permissions.
 
    .DESCRIPTION
        Performs post-restoration verification by reading the current ACL from
        the target object and comparing it with the expected backup data.
        Provides detailed analysis of restoration success and any discrepancies.
 
        NOTE: Verification failures are often expected and normal behavior.
        See troubleshooting documentation for interpreting verification results.
 
    .PARAMETER TargetObjectDN
        Distinguished name of the object to verify.
 
    .PARAMETER ExpectedData
        Backup data containing the expected ACL configuration.
 
    .PARAMETER ToleranceLevel
        Level of tolerance for verification differences: Strict, Standard, or Permissive.
 
    .PARAMETER CorrelationId
        Unique identifier for tracking this verification operation.
 
    .EXAMPLE
        PS> Confirm-RestorationSuccess -TargetObjectDN $dn -ExpectedData $backup
 
        Verifies restoration with standard tolerance for normal environment changes.
 
    .OUTPUTS
        [PSCustomObject] RestorationVerificationResult
    #>


    [CmdletBinding()]
    [OutputType('RestorationVerificationResult')]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$TargetObjectDN,

        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [PSCustomObject]$ExpectedData,

        [Parameter()]
        [ValidateSet('Strict', 'Standard', 'Permissive')]
        [string]$ToleranceLevel = 'Standard',

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

    process {
        try {
            Write-Verbose "Starting restoration verification for $TargetObjectDN - CorrelationId: $CorrelationId"

            # Read current ACL from target object
            $currentAcl = $null
            $currentSddl = $null
            try {
                $currentAcl = Get-Acl -Path "AD:$TargetObjectDN" -ErrorAction Stop
                $currentSddl = $currentAcl.GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::All)
            }
            catch {
                return [PSCustomObject]@{
                    PSTypeName = 'RestorationVerificationResult'
                    IsVerified = $false
                    TargetObjectDN = $TargetObjectDN
                    VerificationIssues = @("Failed to read current ACL: $($_.Exception.Message)")
                    ExpectedEntries = 0
                    ActualEntries = 0
                    MatchedEntries = 0
                    CorrelationId = $CorrelationId
                }
            }

            # Parse ACE entries for comparison
            $expectedAces = @()
            $actualAces = @()

            # Extract ACEs from expected SDDL
            if ($ExpectedData.SDDL) {
                $expectedAces = @($ExpectedData.SDDL -split '\(' | Where-Object { $_ -like '*;*' } | ForEach-Object { "($_" })
            }

            # Extract ACEs from current SDDL
            if ($currentSddl) {
                $actualAces = @($currentSddl -split '\(' | Where-Object { $_ -like '*;*' } | ForEach-Object { "($_" })
            }

            # Compare ACE entries
            $matchedCount = 0
            $verificationIssues = @()

            # Count matches (basic comparison for now)
            foreach ($expectedAce in $expectedAces) {
                if ($actualAces -contains $expectedAce) {
                    $matchedCount++
                }
            }

            # Determine verification success based on tolerance level
            $isVerified = $false
            $toleranceThreshold = switch ($ToleranceLevel) {
                'Strict' { 1.0 }      # 100% match required
                'Standard' { 0.8 }    # 80% match acceptable
                'Permissive' { 0.6 }  # 60% match acceptable
            }

            if ($expectedAces.Count -gt 0) {
                $matchPercentage = $matchedCount / $expectedAces.Count
                $isVerified = $matchPercentage -ge $toleranceThreshold
            } else {
                $isVerified = $actualAces.Count -eq 0
            }

            # Generate verification issues
            if (-not $isVerified) {
                $verificationIssues += "Verification failed with $ToleranceLevel tolerance"
                $verificationIssues += "Expected $($expectedAces.Count) entries, found $($actualAces.Count), matched $matchedCount"

                if (($expectedAces.Count - $actualAces.Count) -gt 3) {
                    $verificationIssues += "Significant entry count difference detected"
                }
            }

            # Note: This is expected behavior - see troubleshooting documentation
            if ($verificationIssues.Count -gt 0) {
                Write-Verbose "Verification differences detected (this is often normal): $($verificationIssues -join '; ') - CorrelationId: $CorrelationId"
            }

            return [PSCustomObject]@{
                PSTypeName = 'RestorationVerificationResult'
                IsVerified = $isVerified
                TargetObjectDN = $TargetObjectDN
                ToleranceLevel = $ToleranceLevel
                VerificationIssues = $verificationIssues
                ExpectedEntries = $expectedAces.Count
                ActualEntries = $actualAces.Count
                MatchedEntries = $matchedCount
                MatchPercentage = if ($expectedAces.Count -gt 0) { [math]::Round(($matchedCount / $expectedAces.Count) * 100, 1) } else { 0 }
                CorrelationId = $CorrelationId
                VerifiedAt = Get-Date
            }
        }
        catch {
            return [PSCustomObject]@{
                PSTypeName = 'RestorationVerificationResult'
                IsVerified = $false
                TargetObjectDN = $TargetObjectDN
                ToleranceLevel = $ToleranceLevel
                VerificationIssues = @("Verification error: $($_.Exception.Message)")
                ExpectedEntries = 0
                ActualEntries = 0
                MatchedEntries = 0
                MatchPercentage = 0
                CorrelationId = $CorrelationId
                VerifiedAt = Get-Date
            }
        }
    }
}


function ConvertFrom-BackupToACL {
    <#
    .SYNOPSIS
        Converts backup SDDL data to Active Directory security descriptor.
 
    .DESCRIPTION
        Converts validated backup SDDL strings into proper Active Directory
        security descriptor objects suitable for ACL application operations.
        Handles SDDL parsing and validation during conversion.
 
    .PARAMETER BackupData
        Validated backup data containing SDDL string.
 
    .PARAMETER CorrelationId
        Unique identifier for tracking this conversion operation.
 
    .EXAMPLE
        PS> ConvertFrom-BackupToACL -BackupData $validatedBackup
 
        Converts backup SDDL to security descriptor object.
 
    .OUTPUTS
        [PSCustomObject] ACLConversionResult
    #>


    [CmdletBinding()]
    [OutputType('ACLConversionResult')]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [ValidateNotNull()]
        [PSCustomObject]$BackupData,

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

    process {
        try {
            if (-not $BackupData.SDDL) {
                return [PSCustomObject]@{
                    PSTypeName = 'ACLConversionResult'
                    Success = $false
                    SecurityDescriptor = $null
                    SourceSDDL = $null
                    ACECount = 0
                    SDDLLength = 0
                    ErrorMessage = "BackupData object does not contain SDDL property"
                    CorrelationId = $CorrelationId
                    ConvertedAt = Get-Date
                }
            }

            # Create security descriptor from SDDL
            $securityDescriptor = [System.DirectoryServices.ActiveDirectorySecurity]::new()
            
            # Try to set SDDL and catch any parsing errors
            try {
                $securityDescriptor.SetSecurityDescriptorSddlForm($BackupData.SDDL)
            }
            catch {
                # SDDL parsing failed - return failure result
                return [PSCustomObject]@{
                    PSTypeName = 'ACLConversionResult'
                    Success = $false
                    SecurityDescriptor = $null
                    SourceSDDL = $BackupData.SDDL
                    ACECount = 0
                    SDDLLength = $BackupData.SDDL.Length
                    ErrorMessage = "SDDL conversion failed: $($_.Exception.Message)"
                    CorrelationId = $CorrelationId
                    ConvertedAt = Get-Date
                }
            }

            # Extract metadata about the ACL
            $aceCount = ($BackupData.SDDL -split '\(' | Where-Object { $_ -like '*;*' }).Count
            $sddlLength = $BackupData.SDDL.Length

            return [PSCustomObject]@{
                PSTypeName = 'ACLConversionResult'
                Success = $true
                SecurityDescriptor = $securityDescriptor
                SourceSDDL = $BackupData.SDDL
                ACECount = $aceCount
                SDDLLength = $sddlLength
                CorrelationId = $CorrelationId
                ConvertedAt = Get-Date
            }
        }
        catch {
            return [PSCustomObject]@{
                PSTypeName = 'ACLConversionResult'
                Success = $false
                SecurityDescriptor = $null
                SourceSDDL = if ($BackupData.SDDL) { $BackupData.SDDL } else { $null }
                ACECount = 0
                SDDLLength = if ($BackupData.SDDL) { $BackupData.SDDL.Length } else { 0 }
                ErrorMessage = "SDDL conversion failed: $($_.Exception.Message)"
                CorrelationId = $CorrelationId
                ConvertedAt = Get-Date
            }
        }
    }
}


#endregion