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 |