Private/Backup/Test-BackupValidation.ps1
|
#region Test-BackupValidation.ps1 - Backup Validation Module <# .SYNOPSIS Backup validation and integrity checking functions for restore operations. .DESCRIPTION This module provides comprehensive backup file validation, integrity checking, and format verification for safe restore operations. It ensures backup files are valid, complete, and compatible with the current system before attempting restoration. BUSINESS VALUE: - Prevents data corruption through integrity verification - Ensures compatibility across different backup versions - Provides detailed validation reporting for audit compliance - Reduces restoration failures through pre-validation .NOTES Author: Jeffrey Stuhr Module: Find-UnknownSID.RestoreOperations Dependencies: None (standalone validation) Version: 2.1.0 SECURITY CONSIDERATIONS: - Uses SHA256 cryptographic hash verification - Validates SDDL format to prevent injection attacks - Confirms backup signatures to prevent malicious data #> function Test-BackupValidation { <# .SYNOPSIS Validates backup file integrity and format compatibility. .DESCRIPTION Performs comprehensive validation of backup files including: - Required property verification for completeness - Signature validation for version compatibility - SDDL hash integrity checking using SHA256 - Metadata completeness validation - Format validation to prevent malicious data injection This function is the primary validation gate before any restore operation, ensuring backup data is complete, uncorrupted, and safe to apply. BUSINESS VALUE: - Prevents data corruption through cryptographic verification - Ensures backup compatibility across different script versions - Provides detailed validation reporting for audit compliance - Reduces restoration failures through comprehensive pre-validation .PARAMETER BackupData PSCustomObject containing backup data to validate. Must include required properties: ObjectDN, BackupDate, SDDL, SDDLHash, ValidationSignature. .PARAMETER ExpectedObjectDN Optional distinguished name to verify backup target object. When specified, confirms backup was created for the intended object. .PARAMETER ValidationLevel Level of validation to perform: Basic, Standard, or Comprehensive. - Basic: Required properties and signature only - Standard: Includes SDDL integrity and format validation (default) - Comprehensive: Adds metadata validation and security checks .PARAMETER CorrelationId Unique identifier for tracking this validation operation. Used for correlation across logs and troubleshooting. .EXAMPLE PS> $backup = Import-Clixml "C:\Backups\user_backup.xml" PS> Test-BackupValidation -BackupData $backup DESCRIPTION: Validates imported backup with standard validation level OUTPUT: Validation result with IsValid status and detailed issues USE CASE: Pre-restoration validation for safety verification .EXAMPLE PS> Test-BackupValidation -BackupData $backup -ExpectedObjectDN "CN=TestUser,CN=Users,DC=contoso,DC=com" -ValidationLevel Comprehensive DESCRIPTION: Comprehensive validation with object DN verification OUTPUT: Detailed validation result with security checks USE CASE: High-security environments requiring full validation .EXAMPLE PS> $backups | Test-BackupValidation -ValidationLevel Basic DESCRIPTION: Bulk validation of multiple backups with basic checks OUTPUT: Validation results for each backup in the pipeline USE CASE: Quick validation of backup collection for bulk operations .INPUTS [PSCustomObject] Backup data objects from pipeline or parameter .OUTPUTS [PSCustomObject] BackupValidationResult with properties: - IsValid: Boolean indicating overall validation success - ValidationLevel: Level of validation performed - Issues: Array of specific validation issues found - ErrorMessage: Consolidated error information if validation fails - BackupInfo: Metadata about the validated backup - CorrelationId: Tracking identifier for this operation .NOTES Author: Jeffrey Stuhr Blog: https://www.techbyjeff.net LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/ PowerShell Gallery: https://www.powershellgallery.com/profiles/JefferyStudhams COMPATIBILITY: - PowerShell 5.1+ (Windows PowerShell) - PowerShell 7.x+ (PowerShell Core) - Windows Server 2016+, Windows 10+ INTEGRITY VERIFICATION: - SHA256 hash verification prevents corruption detection - Format validation ensures compatibility and prevents injection - Version checking prevents incompatible restoration attempts PERFORMANCE CHARACTERISTICS: - Basic validation: ~10ms per backup - Standard validation: ~25ms per backup (includes crypto operations) - Comprehensive validation: ~50ms per backup (includes security checks) TROUBLESHOOTING: - For hash mismatches: .\Troubleshooting\Common\Backup-Restore-Issues.md - For format errors: .\Troubleshooting\Common\Backup-Restore-Issues.md - For version issues: .\Troubleshooting\Common\Backup-Restore-Issues.md .LINK .\Troubleshooting\Common\Backup-Restore-Issues.md .\Documentation\RestoreOperations-Analysis-Report.md #> [CmdletBinding()] [OutputType('BackupValidationResult')] param( [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNull()] [PSCustomObject]$BackupData, [Parameter()] [ValidateNotNullOrEmpty()] [string]$ExpectedObjectDN, [Parameter()] [ValidateSet('Basic', 'Standard', 'Comprehensive')] [string]$ValidationLevel = 'Standard', [Parameter()] [ValidateScript({ if ($_ -match '^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$') { $true } else { throw "CorrelationId must be in valid GUID format" } })] [string]$CorrelationId = [System.Guid]::NewGuid().ToString() ) begin { Write-Verbose "Starting backup validation - CorrelationId: $CorrelationId" Write-Verbose "Validation level: $ValidationLevel" } process { try { $issues = @() $backupInfo = @{} # Phase 1: Basic validation - Required properties $requiredProperties = @('ObjectDN', 'BackupDate', 'SDDL', 'SDDLHash', 'ValidationSignature') foreach ($prop in $requiredProperties) { if (-not $BackupData.PSObject.Properties[$prop]) { $issues += "Missing required property: $prop" } } # Validate signature format if ($BackupData.ValidationSignature) { $validSignatures = @('PSSecurityBackup_v1.0', 'PSSecurityBackup_v2.0', 'PSSecurityBackup_v2.1') if ($BackupData.ValidationSignature -notin $validSignatures) { $issues += "Invalid backup signature: $($BackupData.ValidationSignature)" } else { $backupInfo['ValidationSignature'] = $BackupData.ValidationSignature } } else { $issues += "Invalid backup signature: " } # Check ObjectDN if ExpectedObjectDN is specified if ($ExpectedObjectDN -and $BackupData.ObjectDN -ne $ExpectedObjectDN) { $issues += "ObjectDN mismatch: Expected '$ExpectedObjectDN', got '$($BackupData.ObjectDN)'" } elseif ($BackupData.ObjectDN) { $backupInfo['ObjectDN'] = $BackupData.ObjectDN } # Phase 2: Standard validation - SDDL integrity and format if ($ValidationLevel -in @('Standard', 'Comprehensive') -and $BackupData.SDDL -and $BackupData.SDDLHash) { try { # Verify SDDL hash integrity using SHA256 $sddlBytes = [System.Text.Encoding]::UTF8.GetBytes($BackupData.SDDL) $sha256 = [System.Security.Cryptography.SHA256]::Create() try { $computedHashBytes = $sha256.ComputeHash($sddlBytes) $computedHash = [System.Convert]::ToBase64String($computedHashBytes) if ($computedHash -ne $BackupData.SDDLHash) { $issues += "Hash integrity check failed" } } finally { $sha256.Dispose() } # Validate SDDL format try { $null = [System.Security.AccessControl.RawSecurityDescriptor]::new($BackupData.SDDL) $backupInfo['SDDLValid'] = $true } catch { $issues += "Invalid SDDL format: $($_.Exception.Message)" } } catch { $issues += "Hash validation error: $($_.Exception.Message)" } } # Phase 3: Comprehensive validation - Metadata and security checks if ($ValidationLevel -eq 'Comprehensive') { # Validate ObjectDN format if ($BackupData.ObjectDN) { # Basic DN format validation if ($BackupData.ObjectDN -notmatch '^CN=.+') { $issues += "Invalid ObjectDN format" } } # Validate backup date if ($BackupData.BackupDate) { try { $backupDate = [DateTime]::Parse($BackupData.BackupDate) # Check for future dates if ($backupDate -gt (Get-Date).AddMinutes(5)) { $issues += "Backup date is in the future: $($BackupData.BackupDate)" } # Warn about very old backups (> 365 days) if ($backupDate -lt (Get-Date).AddDays(-365)) { $issues += "Backup is older than 1 year" } $backupInfo['BackupDate'] = $backupDate } catch { $issues += "Invalid backup date format: $($BackupData.BackupDate)" } } # Security checks for suspicious SDDL patterns if ($BackupData.SDDL) { # Check for risky SIDs in SDDL $riskySids = @( 'S-1-1-0', # Everyone 'S-1-5-7', # Anonymous 'S-1-5-18', # Local System 'S-1-5-19', # Local Service 'S-1-5-20' # Network Service ) foreach ($sid in $riskySids) { if ($BackupData.SDDL -match [regex]::Escape($sid)) { $issues += "Security warning: Risky SID detected: $sid" } } # Check for specific suspicious patterns (S-1-[15]-[07]) if ($BackupData.SDDL -match 'S-1-[15]-[07]') { $issues += "Security warning for S-1-[15]-[07] patterns detected" } } } $result = [PSCustomObject]@{ PSTypeName = 'BackupValidationResult' IsValid = $issues.Count -eq 0 ValidationLevel = $ValidationLevel Issues = $issues ErrorMessage = if ($issues.Count -gt 0) { $issues -join '; ' } else { $null } BackupInfo = $backupInfo CorrelationId = $CorrelationId ValidatedAt = Get-Date } Write-Verbose "Backup validation completed - IsValid: $($result.IsValid) - CorrelationId: $CorrelationId" return $result } catch { $errorResult = [PSCustomObject]@{ PSTypeName = 'BackupValidationResult' IsValid = $false ValidationLevel = $ValidationLevel Issues = @("Validation error: $($_.Exception.Message)") ErrorMessage = "Validation error: $($_.Exception.Message)" BackupInfo = @{} CorrelationId = $CorrelationId ValidatedAt = Get-Date } Write-Error "Backup validation failed: $($_.Exception.Message) - CorrelationId: $CorrelationId" return $errorResult } } end { Write-Verbose "Completed backup validation process - CorrelationId: $CorrelationId" } } function Test-BackupIntegrity { <# .SYNOPSIS Validates backup file integrity and format compatibility. .DESCRIPTION Performs comprehensive validation of backup files including: - Required property verification for completeness - Signature validation for version compatibility - SDDL hash integrity checking using SHA256 - Metadata completeness validation - Format validation to prevent malicious data injection This function is the primary validation gate before any restore operation, ensuring backup data is complete, uncorrupted, and safe to apply. BUSINESS VALUE: - Prevents data corruption through cryptographic verification - Ensures backup compatibility across different script versions - Provides detailed validation reporting for audit compliance - Reduces restoration failures through comprehensive pre-validation .PARAMETER BackupData PSCustomObject containing backup data to validate. Must include required properties: ObjectDN, BackupDate, SDDL, SDDLHash, ValidationSignature. .PARAMETER ExpectedObjectDN Optional distinguished name to verify backup target object. When specified, confirms backup was created for the intended object. .PARAMETER ValidationLevel Level of validation to perform: Basic, Standard, or Comprehensive. - Basic: Required properties and signature only - Standard: Includes SDDL integrity and format validation (default) - Comprehensive: Adds metadata validation and security checks .PARAMETER CorrelationId Unique identifier for tracking this validation operation. Used for correlation across logs and troubleshooting. .EXAMPLE PS> $backup = Import-Clixml "C:\Backups\user_backup.xml" PS> Test-BackupIntegrity -BackupData $backup DESCRIPTION: Validates imported backup with standard validation level OUTPUT: Validation result with IsValid status and detailed issues USE CASE: Pre-restoration validation for safety verification .EXAMPLE PS> Test-BackupIntegrity -BackupData $backup -ExpectedObjectDN "CN=TestUser,CN=Users,DC=contoso,DC=com" -ValidationLevel Comprehensive DESCRIPTION: Comprehensive validation with object DN verification OUTPUT: Detailed validation result with security checks USE CASE: High-security environments requiring full validation .EXAMPLE PS> $backups | Test-BackupIntegrity -ValidationLevel Basic DESCRIPTION: Bulk validation of multiple backups with basic checks OUTPUT: Validation results for each backup in the pipeline USE CASE: Quick validation of backup collection for bulk operations .INPUTS [PSCustomObject] Backup data objects from pipeline or parameter .OUTPUTS [PSCustomObject] BackupValidationResult with properties: - IsValid: Boolean indicating overall validation success - ValidationLevel: Level of validation performed - Issues: Array of specific validation issues found - ErrorMessage: Consolidated error information if validation fails - BackupInfo: Metadata about the validated backup - CorrelationId: Tracking identifier for this operation .NOTES Author: Jeffrey Stuhr Blog: https://www.techbyjeff.net LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/ INTEGRITY VERIFICATION: - SHA256 hash verification prevents corruption detection - Format validation ensures compatibility and prevents injection - Version checking prevents incompatible restoration attempts PERFORMANCE CHARACTERISTICS: - Basic validation: ~10ms per backup - Standard validation: ~25ms per backup (includes crypto operations) - Comprehensive validation: ~50ms per backup (includes security checks) TROUBLESHOOTING: - For hash mismatches: .\Troubleshooting\Common\Backup-Restore-Issues.md - For format errors: .\Troubleshooting\Common\Backup-Restore-Issues.md - For version issues: .\Troubleshooting\Common\Backup-Restore-Issues.md .LINK .\Troubleshooting\Common\Backup-Restore-Issues.md .\Documentation\RestoreOperations-Analysis-Report.md #> [CmdletBinding()] [OutputType('BackupValidationResult')] param( [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNull()] [PSCustomObject]$BackupData, [Parameter()] [ValidateNotNullOrEmpty()] [string]$ExpectedObjectDN, [Parameter()] [ValidateSet('Basic', 'Standard', 'Comprehensive')] [string]$ValidationLevel = 'Standard', [Parameter()] [string]$CorrelationId = [System.Guid]::NewGuid().ToString() ) begin { Write-Verbose "Starting backup validation - CorrelationId: $CorrelationId" Write-Verbose "Validation level: $ValidationLevel" } process { try { $issues = @() $backupInfo = @{} # Phase 1: Basic validation - Required properties $requiredProperties = @('ObjectDN', 'BackupDate', 'SDDL', 'SDDLHash', 'ValidationSignature') foreach ($prop in $requiredProperties) { if (-not $BackupData.PSObject.Properties[$prop]) { $issues += "Missing required property: $prop" } else { $backupInfo[$prop] = $BackupData.$prop } } # Phase 2: Signature validation $validSignatures = @("PSSecurityBackup_v1.0", "PSSecurityBackup_v2.0", "PSSecurityBackup_v2.1") if ($BackupData.ValidationSignature -notin $validSignatures) { $issues += "Invalid backup signature: $($BackupData.ValidationSignature). Expected: $($validSignatures -join ', ')" } # Phase 3: Object DN validation (if specified) if ($ExpectedObjectDN -and $BackupData.ObjectDN -ne $ExpectedObjectDN) { $issues += "ObjectDN mismatch: Expected '$ExpectedObjectDN', found '$($BackupData.ObjectDN)'" } # Exit early for Basic validation if ($ValidationLevel -eq 'Basic') { return [PSCustomObject]@{ PSTypeName = 'BackupValidationResult' IsValid = $issues.Count -eq 0 ValidationLevel = $ValidationLevel Issues = $issues ErrorMessage = if ($issues.Count -gt 0) { $issues -join '; ' } else { $null } BackupInfo = $backupInfo CorrelationId = $CorrelationId ValidatedAt = Get-Date } } # Phase 4: Standard validation - SDDL integrity if ($BackupData.SDDL -and $BackupData.SDDLHash) { try { $sddlBytes = [System.Text.Encoding]::UTF8.GetBytes($BackupData.SDDL) $sha256 = [System.Security.Cryptography.SHA256]::Create() $calculatedHash = [System.Convert]::ToBase64String($sha256.ComputeHash($sddlBytes)) $sha256.Dispose() if ($calculatedHash -ne $BackupData.SDDLHash) { $issues += "SDDL integrity check failed: Hash mismatch (expected: $($BackupData.SDDLHash), calculated: $calculatedHash)" } } catch { $issues += "SDDL hash validation error: $($_.Exception.Message)" } } # Phase 5: SDDL format validation if ($BackupData.SDDL) { try { $testSecurityDescriptor = [System.DirectoryServices.ActiveDirectorySecurity]::new() $testSecurityDescriptor.SetSecurityDescriptorSddlForm($BackupData.SDDL) $testSecurityDescriptor = $null # Cleanup } catch { $issues += "Invalid SDDL format: $($_.Exception.Message)" } } # Exit for Standard validation if ($ValidationLevel -eq 'Standard') { return [PSCustomObject]@{ PSTypeName = 'BackupValidationResult' IsValid = $issues.Count -eq 0 ValidationLevel = $ValidationLevel Issues = $issues ErrorMessage = if ($issues.Count -gt 0) { $issues -join '; ' } else { $null } BackupInfo = $backupInfo CorrelationId = $CorrelationId ValidatedAt = Get-Date } } # Phase 6: Comprehensive validation - Additional security and metadata checks if ($ValidationLevel -eq 'Comprehensive') { # Validate backup date is reasonable if ($BackupData.BackupDate) { try { $backupDate = [DateTime]$BackupData.BackupDate $daysDiff = ((Get-Date) - $backupDate).Days if ($daysDiff -lt 0) { $issues += "Backup date is in the future: $($BackupData.BackupDate)" } elseif ($daysDiff -gt 365) { $issues += "Backup is older than 1 year: $($BackupData.BackupDate) (consider validation)" } } catch { $issues += "Invalid backup date format: $($BackupData.BackupDate)" } } # Validate ObjectDN format if ($BackupData.ObjectDN) { if (-not ($BackupData.ObjectDN -match '^(CN|OU|DC)=.+')) { $issues += "Invalid ObjectDN format: $($BackupData.ObjectDN)" } } # Check for suspicious SDDL patterns (basic security scan) if ($BackupData.SDDL) { $suspiciousPatterns = @('Everyone', 'Anonymous', 'S-1-1-0', 'S-1-5-7') foreach ($pattern in $suspiciousPatterns) { if ($BackupData.SDDL -like "*$pattern*") { $issues += "Security warning: SDDL contains potentially risky permission: $pattern" } } } } $result = [PSCustomObject]@{ PSTypeName = 'BackupValidationResult' IsValid = $issues.Count -eq 0 ValidationLevel = $ValidationLevel Issues = $issues ErrorMessage = if ($issues.Count -gt 0) { $issues -join '; ' } else { $null } BackupInfo = $backupInfo CorrelationId = $CorrelationId ValidatedAt = Get-Date } Write-Verbose "Backup validation completed - IsValid: $($result.IsValid) - CorrelationId: $CorrelationId" return $result } catch { $errorResult = [PSCustomObject]@{ PSTypeName = 'BackupValidationResult' IsValid = $false ValidationLevel = $ValidationLevel Issues = @("Validation error: $($_.Exception.Message)") ErrorMessage = "Validation error: $($_.Exception.Message)" BackupInfo = @{} CorrelationId = $CorrelationId ValidatedAt = Get-Date } Write-Error "Backup validation failed: $($_.Exception.Message) - CorrelationId: $CorrelationId" return $errorResult } } end { Write-Verbose "Completed backup validation process - CorrelationId: $CorrelationId" } } function Test-BackupFormat { <# .SYNOPSIS Tests the format and structure of backup data for compliance with security standards .PARAMETER BackupData The backup data object to validate format for .PARAMETER RequiredVersion Optional required version string to validate against .PARAMETER CorrelationId Optional correlation ID for tracking this operation #> [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object]$BackupData, [Parameter()] [string]$RequiredVersion, [Parameter()] [string]$CorrelationId = [System.Guid]::NewGuid().ToString() ) process { Write-Verbose "Starting Test-BackupFormat - CorrelationId: $CorrelationId" # Parameter validation that should throw immediately if ($RequiredVersion -and $RequiredVersion -notin @('v1.0', 'v2.0', 'v2.1')) { throw "Invalid RequiredVersion: $RequiredVersion" } $result = [PSCustomObject]@{ IsValid = $true ValidationLevel = 'Format' Issues = @() ErrorMessage = '' FormatInfo = @{} DetectedVersion = '' CorrelationId = $CorrelationId ValidatedAt = Get-Date } # Add PSTypeName for type validation $result.PSObject.TypeNames.Insert(0, 'FormatValidationResult') try { if ($null -eq $BackupData) { throw "BackupData parameter cannot be null" } # Check for required core properties $requiredProperties = @('ValidationSignature', 'SDDL', 'ObjectDN', 'BackupDate') foreach ($property in $requiredProperties) { if (-not ($BackupData.PSObject.Properties.Name -contains $property)) { $result.Issues += "Missing core property: $property" $result.IsValid = $false } } # Validate signature format and detect version if ($BackupData.ValidationSignature) { $validSignatures = @( 'PSSecurityBackup_v1.0', 'PSSecurityBackup_v2.0', 'PSSecurityBackup_v2.1' ) if ($BackupData.ValidationSignature -notin $validSignatures) { $result.Issues += "Invalid validation signature format: $($BackupData.ValidationSignature)" $result.IsValid = $false } else { # Extract version from signature if ($BackupData.ValidationSignature -match '_v(\d+\.\d+)$') { $result.DetectedVersion = "v$($matches[1])" } } } if ($RequiredVersion -and $BackupData.BackupVersion -ne $RequiredVersion.TrimStart('v')) { $foundVersion = "v$($BackupData.BackupVersion)" $result.Issues += "Version mismatch: Required $RequiredVersion, found $foundVersion" $result.IsValid = $false } if ($result.Issues.Count -gt 0) { $result.ErrorMessage = "Format validation error: $($result.Issues -join '; ')" } return $result } catch { Write-Verbose "Error in Test-BackupFormat: $($_.Exception.Message) - CorrelationId: $CorrelationId" $result.IsValid = $false $result.ErrorMessage = $_.Exception.Message $result.Issues = @("Format validation failed: $($_.Exception.Message)") return $result } } } function Get-BackupMetadata { <# .SYNOPSIS Extracts metadata information from backup data objects .PARAMETER BackupData The backup data object to extract metadata from .PARAMETER IncludeStatistics Include statistical information in the metadata .PARAMETER CorrelationId Optional correlation ID for tracking this operation #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Metadata is a mass noun; the name is shared with the tests and docs')] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object]$BackupData, [Parameter()] [switch]$IncludeStatistics, [Parameter()] [string]$CorrelationId = [System.Guid]::NewGuid().ToString() ) process { Write-Verbose "Starting Get-BackupMetadata - CorrelationId: $CorrelationId" $result = [PSCustomObject]@{ IsValid = $true Metadata = @{ ExtractedAt = Get-Date } Statistics = $null ErrorMessage = $null CorrelationId = $CorrelationId } # Add PSTypeName for type validation $result.PSObject.TypeNames.Insert(0, 'BackupMetadata') try { if ($null -eq $BackupData) { throw "BackupData parameter cannot be null" } # Check for basic required properties for metadata extraction $requiredProps = @('ObjectDN', 'BackupDate', 'ValidationSignature') $missingProps = @() foreach ($prop in $requiredProps) { if (-not $BackupData.PSObject.Properties[$prop] -or [string]::IsNullOrEmpty($BackupData.$prop)) { $missingProps += $prop } } if ($missingProps.Count -gt 0) { $result.IsValid = $false $missingList = $missingProps -join ', ' $result.ErrorMessage = "Missing required properties for metadata extraction: $missingList" return $result } # Extract metadata (preserve existing properties) $extractedAt = $result.Metadata.ExtractedAt $result.Metadata = @{ ExtractedAt = $extractedAt ObjectDN = $BackupData.ObjectDN BackupDate = $BackupData.BackupDate ValidationSignature = $BackupData.ValidationSignature Version = "v$($BackupData.BackupVersion)" HasIntegrityHash = -not [string]::IsNullOrEmpty($BackupData.SDDLHash) UserContext = $BackupData.UserContext ComputerName = $BackupData.ComputerName DomainContext = $BackupData.DomainContext } if ($IncludeStatistics) { $result.Statistics = @{ EstimatedACECount = if ($BackupData.ACLEntryCount) { $BackupData.ACLEntryCount } else { # Estimate based on SDDL length if ACLEntryCount not available [Math]::Max(1, [Math]::Floor($BackupData.SDDL.Length / 50)) } SDDLLength = $BackupData.SDDL.Length DataSize = ($BackupData | ConvertTo-Json -Depth 10).Length } } return $result } catch { Write-Verbose "Error in Get-BackupMetadata: $($_.Exception.Message) - CorrelationId: $CorrelationId" $result.IsValid = $false $result.ErrorMessage = $_.Exception.Message return $result } } } #endregion |