From fc7380c1ba724ace04f7326d283607e8b7fb182e Mon Sep 17 00:00:00 2001 From: Chris Howarth Date: Sun, 6 Sep 2026 11:18:18 +0100 Subject: [PATCH] Invoke-DbaDbDecryptObject - Read the family GUID from sys.database_recovery_status instead of DBCC DBINFO The DataPages method needed the database family GUID to rebuild the keystream and scraped it out of DBCC DBINFO's text dump under trace flag 3604. It now reads family_guid from the catalog view sys.database_recovery_status, a typed uniqueidentifier present since SQL Server 2005. The two sources were measured byte for byte across SQL Server 2019, 2022 and 2025, for user databases and master, and always agree, so the keystream input is unchanged while a trace flag toggle and a regex over prose are dropped. DBCC PAGE still reads the ciphertext and still needs sysadmin, so the sysadmin check stays; the catalog view needs only VIEW SERVER STATE, which a sysadmin already holds. (do Invoke-DbaDbDecryptObject) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Get-EncryptedObjectKeystream.ps1 | 4 ++-- public/Invoke-DbaDbDecryptObject.ps1 | 20 ++++++++++++------- tests/Invoke-DbaDbDecryptObject.Tests.ps1 | 8 ++++---- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/private/functions/Get-EncryptedObjectKeystream.ps1 b/private/functions/Get-EncryptedObjectKeystream.ps1 index a9a170612e3..a795d8644c1 100644 --- a/private/functions/Get-EncryptedObjectKeystream.ps1 +++ b/private/functions/Get-EncryptedObjectKeystream.ps1 @@ -22,8 +22,8 @@ function Get-EncryptedObjectKeystream { - Invoke-DbaDbDecryptObject .PARAMETER FamilyGuid - The family GUID of the database that holds the object, as reported by dbi_familyGUID of DBCC DBINFO. - One value per database, stable for the life of the database. + The family GUID of the database that holds the object, as reported by family_guid of the catalog view + sys.database_recovery_status. One value per database, stable for the life of the database. .PARAMETER ObjectId The object id of the encrypted object. diff --git a/public/Invoke-DbaDbDecryptObject.ps1 b/public/Invoke-DbaDbDecryptObject.ps1 index 1f87d5bb20a..9c34c16ede8 100644 --- a/public/Invoke-DbaDbDecryptObject.ps1 +++ b/public/Invoke-DbaDbDecryptObject.ps1 @@ -305,8 +305,9 @@ function Invoke-DbaDbDecryptObject { Stop-Function -Message "Reading the encrypted objects on $instance without a dedicated admin connection uses DBCC PAGE, which $azurePlatform does not support." -Target $instance -Continue } - # DBCC PAGE and DBCC DBINFO are limited to sysadmin, so checking up front gives a clear - # message instead of a permission error in the middle of reading the pages. + # DBCC PAGE is limited to sysadmin, so checking up front gives a clear message instead of + # a permission error in the middle of reading the pages. sys.database_recovery_status, which + # supplies the family GUID, needs only VIEW SERVER STATE, which a sysadmin already holds. $querySysadmin = @" SELECT IS_SRVROLEMEMBER('sysadmin') AS IsSysadmin "@ @@ -450,16 +451,21 @@ AND p.is_ms_shipped = 0 $familyGuid = $null $imageValueMap = @{ } + # The family GUID comes from the catalog view sys.database_recovery_status, a typed + # uniqueidentifier present since SQL Server 2005, rather than by scraping dbi_familyGUID + # out of DBCC DBINFO's text dump. The two were measured byte for byte across SQL Server + # 2019, 2022 and 2025, for user databases and master, and always agree, so the swap keeps + # the keystream input identical while dropping a trace flag toggle and a regex over prose. try { - $familyGuidRow = @($db.Query("DBCC DBINFO WITH TABLERESULTS") | Where-Object Field -eq "dbi_familyGUID") + $familyGuidRow = @($db.Query("SELECT family_guid FROM sys.database_recovery_status WHERE database_id = DB_ID()")) } catch { - Stop-Function -Message "Couldn't read dbi_familyGUID of database $($db.Name) on $instance" -ErrorRecord $_ -Target $instance -Continue + Stop-Function -Message "Couldn't read the family GUID of database $($db.Name) on $instance" -ErrorRecord $_ -Target $instance -Continue } - if ($familyGuidRow.Count -eq 0) { - Stop-Function -Message "Couldn't read dbi_familyGUID of database $($db.Name) on $instance" -Target $instance -Continue + if ($familyGuidRow.Count -eq 0 -or $null -eq $familyGuidRow[0].family_guid -or $familyGuidRow[0].family_guid -is [System.DBNull]) { + Stop-Function -Message "Couldn't read the family GUID of database $($db.Name) on $instance" -Target $instance -Continue } - $familyGuid = [guid]$familyGuidRow[0].VALUE + $familyGuid = [guid]$familyGuidRow[0].family_guid $wantedObjectId = @($objectCollection.ID) diff --git a/tests/Invoke-DbaDbDecryptObject.Tests.ps1 b/tests/Invoke-DbaDbDecryptObject.Tests.ps1 index d66375c58ce..680472f274d 100644 --- a/tests/Invoke-DbaDbDecryptObject.Tests.ps1 +++ b/tests/Invoke-DbaDbDecryptObject.Tests.ps1 @@ -723,8 +723,8 @@ SELECT 'áéíñóú¡¿' as SampleUTF8;" $routeText = InModuleScope dbatools -Parameters @{ DatabaseName = $dbname; Instance = $TestConfig.InstanceMulti1 } { $server = Connect-DbaInstance -SqlInstance $Instance $db = $server.Databases[$DatabaseName] - $familyGuidRow = @($db.Query("DBCC DBINFO WITH TABLERESULTS") | Where-Object Field -eq "dbi_familyGUID") - $familyGuid = [guid]$familyGuidRow[0].VALUE + $familyGuidRow = @($db.Query("SELECT family_guid FROM sys.database_recovery_status WHERE database_id = DB_ID()")) + $familyGuid = [guid]$familyGuidRow[0].family_guid $encryptedId = @(@($db.Query("SELECT m.object_id AS ObjectId FROM sys.sql_modules AS m WHERE m.definition IS NULL")) | ForEach-Object { [int]$PSItem.ObjectId }) $result = @{ } @@ -773,8 +773,8 @@ SELECT 'áéíñóú¡¿' as SampleUTF8;" $scanText = InModuleScope dbatools -Parameters @{ DatabaseName = $dbname; Instance = $TestConfig.InstanceMulti1 } { $server = Connect-DbaInstance -SqlInstance $Instance $db = $server.Databases[$DatabaseName] - $familyGuidRow = @($db.Query("DBCC DBINFO WITH TABLERESULTS") | Where-Object Field -eq "dbi_familyGUID") - $familyGuid = [guid]$familyGuidRow[0].VALUE + $familyGuidRow = @($db.Query("SELECT family_guid FROM sys.database_recovery_status WHERE database_id = DB_ID()")) + $familyGuid = [guid]$familyGuidRow[0].family_guid $encryptedId = @($db.Query("SELECT m.object_id AS ObjectId FROM sys.sql_modules AS m WHERE m.definition IS NULL")) | ForEach-Object { [int]$PSItem.ObjectId }