Private/Security/Get-SecurityDescriptor.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Security Descriptor Retrieval Module for Find-UnknownSID Solution .DESCRIPTION This focused module handles all security descriptor retrieval and ACL processing scenarios for the Find-UnknownSID enterprise solution. It implements multiple retrieval strategies with fallback mechanisms to ensure reliable access to security descriptors across various Active Directory object types and states. SINGLE RESPONSIBILITY: This module does ONE thing well: Retrieve and process security descriptors from Active Directory objects using multiple strategies for maximum reliability. BUSINESS VALUE: - Reliable security descriptor access across different object states - Robust handling of deserialized objects from background jobs - Comprehensive fallback mechanisms for production environments - Optimized processing for large-scale AD environments TECHNICAL FEATURES: - Multiple security descriptor retrieval strategies (Get-Acl, binary, fresh query) - Support for deserialized PowerShell objects from background jobs - Intelligent format detection and conversion - Memory-efficient processing with proper cleanup - Comprehensive error handling and logging .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 ACL retrieval failures: .\Troubleshooting\Security\Security-Descriptor-Issues.md - For binary processing errors: .\Troubleshooting\Security\Binary-Descriptor-Processing.md - For performance optimization: .\Troubleshooting\Performance\Security-Descriptor-Performance.md DEPENDENCIES: - Requires Logging.ps1 for Write-StructuredLog function - Uses script-scoped variables (CorrelationId for tracking) #> #region Security Descriptor Retrieval Functions function Get-ObjectAccessRule { <# .SYNOPSIS Retrieves access rules from AD object security descriptors using multiple strategies .DESCRIPTION Orchestrates the retrieval of ACL access rules from Active Directory objects using multiple fallback methods to ensure reliable access across various scenarios including deserialized objects, binary descriptors, and fresh queries. This function coordinates three retrieval strategies: 1. Get-Acl method (preferred for reliability) 2. Binary security descriptor processing (handles various formats) 3. Fresh AD query (last resort fallback) .PARAMETER ADObject Active Directory object containing security descriptor. Can be from Get-ADObject output, background job results, or pipeline input. .PARAMETER IncludeInherited When specified, includes inherited ACL entries in the results. By default, only explicit (non-inherited) ACL entries are returned. .PARAMETER CorrelationId Unique identifier for tracking this operation across logs and audit trails. Generated automatically if not provided. .EXAMPLE PS> $accessRules = Get-ObjectAccessRule -ADObject $adObject DESCRIPTION: Basic ACL retrieval for explicit permissions only OUTPUT: Array of ActiveDirectoryAccessRule objects USE CASE: Standard security analysis of explicit permissions .EXAMPLE PS> $allRules = Get-ObjectAccessRule -ADObject $adObject -IncludeInherited DESCRIPTION: Comprehensive ACL retrieval including inherited permissions OUTPUT: Complete set of access rules including inheritance BUSINESS CASE: Thorough security audit requiring all permission sources .EXAMPLE PS> $jobResults | Get-ObjectAccessRule -CorrelationId $correlationId DESCRIPTION: Processing deserialized objects from background jobs OUTPUT: Access rules extracted from job results with correlation tracking INTEGRATION: Large-scale parallel processing scenarios .OUTPUTS [System.DirectoryServices.ActiveDirectoryAccessRule[]] Array of access rules extracted from the security descriptor, or null if retrieval fails .NOTES RETRIEVAL STRATEGY: 1. Get-Acl Method: Uses PowerShell's Get-Acl for standard retrieval 2. Binary Processing: Handles binary, Base64, and deserialized formats 3. Fresh Query: Performs new AD query as last resort PERFORMANCE CHARACTERISTICS: - Get-Acl: Fastest and most reliable for standard objects - Binary Processing: Handles complex scenarios (background jobs) - Fresh Query: Slowest but most comprehensive fallback ERROR HANDLING: - Each method includes comprehensive error handling - Graceful fallback between methods - Detailed logging for troubleshooting - Memory cleanup after processing COMPATIBILITY: - Supports PowerShell 5.1 and 7.x environments - Handles deserialized objects from background jobs - Works with various security descriptor formats #> [CmdletBinding()] [OutputType([System.DirectoryServices.ActiveDirectoryAccessRule[]])] param( [Parameter(Mandatory, ValueFromPipeline)] [PSObject]$ADObject, [Parameter()] [switch]$IncludeInherited, [Parameter()] [string]$CorrelationId = [System.Guid]::NewGuid().ToString() ) process { try { $objectDN = $ADObject.DistinguishedName Write-StructuredLog "Starting security descriptor retrieval for: $objectDN (IncludeInherited: $IncludeInherited)" -Level Debug -CorrelationId $CorrelationId # Strategy 1: Try Get-Acl method (preferred for reliability) $accessRules = Get-SecurityDescriptorFromGetAcl -ADObject $ADObject -IncludeInherited:$IncludeInherited -CorrelationId $CorrelationId if ($accessRules) { Write-StructuredLog "Successfully retrieved access rules using Get-Acl method for $objectDN" -Level Debug -CorrelationId $CorrelationId return $accessRules } # Strategy 2: Try binary/deserialized processing $accessRules = Get-SecurityDescriptorFromBinary -ADObject $ADObject -IncludeInherited:$IncludeInherited -CorrelationId $CorrelationId if ($accessRules) { Write-StructuredLog "Successfully retrieved access rules using binary processing for $objectDN" -Level Debug -CorrelationId $CorrelationId return $accessRules } # Strategy 3: Last resort - fresh AD query $accessRules = Invoke-FreshADQuery -ADObject $ADObject -IncludeInherited:$IncludeInherited -CorrelationId $CorrelationId if ($accessRules) { Write-StructuredLog "Successfully retrieved access rules using fresh AD query for $objectDN" -Level Debug -CorrelationId $CorrelationId return $accessRules } # All methods failed Write-StructuredLog "All security descriptor retrieval methods failed for $objectDN" -Level Warning -CorrelationId $CorrelationId return $null } catch { Write-StructuredLog "Unexpected error retrieving access rules for $($ADObject.DistinguishedName): $($_.Exception.Message)" -Level Error -CorrelationId $CorrelationId return $null } } } function Get-SecurityDescriptorFromGetAcl { <# .SYNOPSIS Retrieves security descriptor using PowerShell's Get-Acl cmdlet .DESCRIPTION Uses the Get-Acl cmdlet to retrieve security descriptors from AD objects. This is the preferred method for standard objects as it's the most reliable and performant approach. .PARAMETER ADObject Active Directory object containing security descriptor .PARAMETER IncludeInherited Whether to include inherited ACL entries .PARAMETER CorrelationId Unique identifier for tracking this operation .OUTPUTS [System.DirectoryServices.ActiveDirectoryAccessRule[]] Array of access rules or null if Get-Acl fails .NOTES METHOD CHARACTERISTICS: - Fastest and most reliable for standard AD objects - Direct PowerShell cmdlet approach - Automatic handling of AD provider context - Best error handling and logging support #> [CmdletBinding()] [OutputType([System.DirectoryServices.ActiveDirectoryAccessRule[]])] param( [Parameter(Mandatory)] [PSObject]$ADObject, [Parameter()] [switch]$IncludeInherited, [Parameter()] [string]$CorrelationId = [System.Guid]::NewGuid().ToString() ) try { $objectDN = $ADObject.DistinguishedName Write-StructuredLog "Attempting Get-Acl retrieval for: $objectDN" -Level Debug -CorrelationId $CorrelationId $acl = Get-Acl -Path "AD:\$objectDN" -ErrorAction Stop if (-not $acl -or -not $acl.Access) { Write-StructuredLog "Get-Acl returned null or empty access list for $objectDN" -Level Debug -CorrelationId $CorrelationId return $null } $accessRules = if ($IncludeInherited) { $acl.Access } else { $acl.Access | Where-Object { -not $_.IsInherited } } Write-StructuredLog "Get-Acl successfully retrieved $($accessRules.Count) access rules for $objectDN" -Level Debug -CorrelationId $CorrelationId return $accessRules } catch { Write-StructuredLog "Get-Acl failed for $($ADObject.DistinguishedName): $($_.Exception.Message)" -Level Debug -CorrelationId $CorrelationId return $null } } function Get-SecurityDescriptorFromBinary { <# .SYNOPSIS Processes security descriptors from binary data and deserialized objects .DESCRIPTION Handles various security descriptor formats including binary data, Base64 strings, and deserialized objects from PowerShell background jobs. This method provides robust processing for complex scenarios. .PARAMETER ADObject Active Directory object containing security descriptor .PARAMETER IncludeInherited Whether to include inherited ACL entries .PARAMETER CorrelationId Unique identifier for tracking this operation .OUTPUTS [System.DirectoryServices.ActiveDirectoryAccessRule[]] Array of access rules or null if binary processing fails .NOTES SUPPORTED FORMATS: - Deserialized ActiveDirectorySecurity objects (from background jobs) - Regular ActiveDirectorySecurity objects - Binary byte arrays - Base64 encoded strings - Generic byte collections PROCESSING STRATEGY: 1. Detect format of security descriptor 2. Route to appropriate processing function 3. Extract and filter access rules 4. Clean up references for memory management #> [CmdletBinding()] [OutputType([System.DirectoryServices.ActiveDirectoryAccessRule[]])] param( [Parameter(Mandatory)] [PSObject]$ADObject, [Parameter()] [switch]$IncludeInherited, [Parameter()] [string]$CorrelationId = [System.Guid]::NewGuid().ToString() ) try { $objectDN = $ADObject.DistinguishedName $binarySD = $ADObject.nTSecurityDescriptor if (-not $binarySD) { Write-StructuredLog "No nTSecurityDescriptor property found for $objectDN" -Level Debug -CorrelationId $CorrelationId return $null } Write-StructuredLog "Processing binary security descriptor for: $objectDN" -Level Debug -CorrelationId $CorrelationId # Check for deserialized ActiveDirectorySecurity object (from background jobs) if ($binarySD.PSObject.TypeNames[0] -like '*Deserialized*ActiveDirectorySecurity*') { return Get-SecurityDescriptorFromDeserialized -SecurityDescriptor $binarySD -IncludeInherited:$IncludeInherited -ObjectDN $objectDN -CorrelationId $CorrelationId } # Check for regular ActiveDirectorySecurity object if ($binarySD -is [System.DirectoryServices.ActiveDirectorySecurity]) { Write-StructuredLog "Processing regular ActiveDirectorySecurity object for $objectDN" -Level Debug -CorrelationId $CorrelationId $accessRules = if ($IncludeInherited) { $binarySD.Access } else { $binarySD.Access | Where-Object { -not $_.IsInherited } } Write-StructuredLog "Successfully processed ActiveDirectorySecurity object for $objectDN - Found $($accessRules.Count) rules" -Level Debug -CorrelationId $CorrelationId return $accessRules } # Process as binary data return ConvertTo-SecurityDescriptor -BinaryData $binarySD -IncludeInherited:$IncludeInherited -ObjectDN $objectDN -CorrelationId $CorrelationId } catch { Write-StructuredLog "Binary security descriptor processing failed for $($ADObject.DistinguishedName): $($_.Exception.Message)" -Level Debug -CorrelationId $CorrelationId return $null } } function Get-SecurityDescriptorFromDeserialized { <# .SYNOPSIS Processes deserialized ActiveDirectorySecurity objects from background jobs .DESCRIPTION Handles the specific case of deserialized ActiveDirectorySecurity objects that result from PowerShell background job processing. These objects require special handling due to their serialized nature. .PARAMETER SecurityDescriptor Deserialized ActiveDirectorySecurity object .PARAMETER IncludeInherited Whether to include inherited ACL entries .PARAMETER ObjectDN Distinguished name for logging context .PARAMETER CorrelationId Unique identifier for tracking this operation .OUTPUTS [System.DirectoryServices.ActiveDirectoryAccessRule[]] Array of access rules or null if deserialized processing fails #> [CmdletBinding()] [OutputType([System.DirectoryServices.ActiveDirectoryAccessRule[]])] param( [Parameter(Mandatory)] [PSObject]$SecurityDescriptor, [Parameter()] [switch]$IncludeInherited, [Parameter()] [string]$ObjectDN, [Parameter()] [string]$CorrelationId = [System.Guid]::NewGuid().ToString() ) try { Write-StructuredLog "Processing deserialized ActiveDirectorySecurity object for $ObjectDN" -Level Debug -CorrelationId $CorrelationId if (-not $SecurityDescriptor.Access) { Write-StructuredLog "Deserialized security descriptor has no Access property for $ObjectDN" -Level Debug -CorrelationId $CorrelationId return $null } $accessRules = if ($IncludeInherited) { $SecurityDescriptor.Access } else { $SecurityDescriptor.Access | Where-Object { -not $_.IsInherited } } Write-StructuredLog "Successfully extracted $($accessRules.Count) access rules from deserialized security descriptor for $ObjectDN" -Level Debug -CorrelationId $CorrelationId return $accessRules } catch { Write-StructuredLog "Failed to process deserialized security descriptor for $ObjectDN : $($_.Exception.Message)" -Level Warning -CorrelationId $CorrelationId return $null } } function ConvertTo-SecurityDescriptor { <# .SYNOPSIS Converts binary data to ActiveDirectorySecurity objects .DESCRIPTION Handles conversion of various binary formats (byte arrays, Base64 strings) to ActiveDirectorySecurity objects for ACL processing. .PARAMETER BinaryData Binary security descriptor data in various formats .PARAMETER IncludeInherited Whether to include inherited ACL entries .PARAMETER ObjectDN Distinguished name for logging context .PARAMETER CorrelationId Unique identifier for tracking this operation .OUTPUTS [System.DirectoryServices.ActiveDirectoryAccessRule[]] Array of access rules or null if conversion fails #> [CmdletBinding()] [OutputType([System.DirectoryServices.ActiveDirectoryAccessRule[]])] param( [Parameter(Mandatory)] [PSObject]$BinaryData, [Parameter()] [switch]$IncludeInherited, [Parameter()] [string]$ObjectDN, [Parameter()] [string]$CorrelationId = [System.Guid]::NewGuid().ToString() ) try { Write-StructuredLog "Converting binary data to security descriptor for $ObjectDN" -Level Debug -CorrelationId $CorrelationId $securityDescriptor = [System.DirectoryServices.ActiveDirectorySecurity]::new() # Handle different binary data formats if ($BinaryData -is [byte[]]) { Write-StructuredLog "Processing byte array security descriptor" -Level Debug -CorrelationId $CorrelationId $securityDescriptor.SetSecurityDescriptorBinaryForm($BinaryData) } elseif ($BinaryData -is [string]) { Write-StructuredLog "Processing Base64 string security descriptor" -Level Debug -CorrelationId $CorrelationId $byteArray = [System.Convert]::FromBase64String($BinaryData) $securityDescriptor.SetSecurityDescriptorBinaryForm($byteArray) } else { Write-StructuredLog "Converting generic binary data to byte array" -Level Debug -CorrelationId $CorrelationId $byteArray = [byte[]]$BinaryData $securityDescriptor.SetSecurityDescriptorBinaryForm($byteArray) } $accessRules = if ($IncludeInherited) { $securityDescriptor.Access } else { $securityDescriptor.Access | Where-Object { -not $_.IsInherited } } Write-StructuredLog "Successfully converted binary security descriptor for $ObjectDN - Found $($accessRules.Count) rules" -Level Debug -CorrelationId $CorrelationId # Clean up security descriptor reference $securityDescriptor = $null return $accessRules } catch { Write-StructuredLog "Failed to convert binary security descriptor for $ObjectDN : $($_.Exception.Message)" -Level Warning -CorrelationId $CorrelationId return $null } } function Invoke-FreshADQuery { <# .SYNOPSIS Performs fresh Active Directory query as last resort for security descriptor retrieval .DESCRIPTION When other methods fail, this function performs a fresh query to Active Directory to retrieve the security descriptor. This is the most comprehensive but slowest method and should only be used as a last resort. .PARAMETER ADObject Active Directory object for fresh query .PARAMETER IncludeInherited Whether to include inherited ACL entries .PARAMETER CorrelationId Unique identifier for tracking this operation .OUTPUTS [System.DirectoryServices.ActiveDirectoryAccessRule[]] Array of access rules or null if fresh query fails .NOTES PERFORMANCE IMPACT: - This method is the slowest as it performs a new AD query - Should only be used when other methods fail - Includes comprehensive error handling - Implements proper resource cleanup #> [CmdletBinding()] [OutputType([System.DirectoryServices.ActiveDirectoryAccessRule[]])] param( [Parameter(Mandatory)] [PSObject]$ADObject, [Parameter()] [switch]$IncludeInherited, [Parameter()] [string]$CorrelationId = [System.Guid]::NewGuid().ToString() ) try { $objectDN = $ADObject.DistinguishedName Write-StructuredLog "Attempting fresh AD query for security descriptor: $objectDN" -Level Debug -CorrelationId $CorrelationId $freshObj = Get-ADObject -Identity $objectDN -Properties nTSecurityDescriptor -ErrorAction Stop if (-not $freshObj.nTSecurityDescriptor) { Write-StructuredLog "Fresh AD query returned no security descriptor for $objectDN" -Level Warning -CorrelationId $CorrelationId return $null } # Process the fresh security descriptor if ($freshObj.nTSecurityDescriptor -is [System.DirectoryServices.ActiveDirectorySecurity]) { $securityDescriptor = $freshObj.nTSecurityDescriptor } else { $securityDescriptor = [System.DirectoryServices.ActiveDirectorySecurity]::new() if ($freshObj.nTSecurityDescriptor -is [byte[]]) { $securityDescriptor.SetSecurityDescriptorBinaryForm($freshObj.nTSecurityDescriptor) } else { $byteArray = [byte[]]$freshObj.nTSecurityDescriptor $securityDescriptor.SetSecurityDescriptorBinaryForm($byteArray) } } $accessRules = if ($IncludeInherited) { $securityDescriptor.Access } else { $securityDescriptor.Access | Where-Object { -not $_.IsInherited } } Write-StructuredLog "Fresh AD query successfully retrieved $($accessRules.Count) access rules for $objectDN" -Level Debug -CorrelationId $CorrelationId # Clean up security descriptor reference $securityDescriptor = $null return $accessRules } catch { Write-StructuredLog "Fresh AD query failed for $($ADObject.DistinguishedName): $($_.Exception.Message)" -Level Warning -CorrelationId $CorrelationId return $null } } #endregion |