From 7f7bc1b24e1986e4f138acda2c072d3ee8e142aa Mon Sep 17 00:00:00 2001 From: jpomfret Date: Mon, 24 Mar 2025 16:19:40 +0000 Subject: [PATCH 1/3] lets check full backups --- containers/JessAndBeard.psm1 | 7 ++++ source/checks/Databasev5.Tests.ps1 | 36 ++++++++++++++++++- .../internal/configurations/configuration.ps1 | 8 +++-- .../functions/Get-AllDatabaseInfo.ps1 | 14 ++++++-- 4 files changed, 60 insertions(+), 5 deletions(-) diff --git a/containers/JessAndBeard.psm1 b/containers/JessAndBeard.psm1 index 20a7a393..f77ded8c 100644 --- a/containers/JessAndBeard.psm1 +++ b/containers/JessAndBeard.psm1 @@ -2379,6 +2379,13 @@ The Tags are the same" PassedChange = 0 # + or - the number of tests passed for v5 FailedChange = 0 # + or - the number of tests failed for v5 SkippedChange = -3 # + or - the number of tests skipped for v5 + }, + @{ + Name = 'LastFullBackup' + RunChange = +1 # + or - the number of tests run for v5 + PassedChange = 0 # + or - the number of tests passed for v5 + FailedChange = 0 # + or - the number of tests failed for v5 + SkippedChange = +1 # + or - the number of tests skipped for v5 } ) $runchange = 0 diff --git a/source/checks/Databasev5.Tests.ps1 b/source/checks/Databasev5.Tests.ps1 index bea0c23d..00f13e69 100644 --- a/source/checks/Databasev5.Tests.ps1 +++ b/source/checks/Databasev5.Tests.ps1 @@ -307,7 +307,7 @@ Describe "Page Verify" -Tag PageVerify, Medium, Database -ForEach $InstancesToTe Describe "Foreign keys and check constraints not trusted" -Tag FKCKTrusted, Low, Database -ForEach $InstancesToTest { $Skip = ($__dbcconfig | Where-Object Name -EQ 'skip.database.fkcktrusted').Value - Context "Testing Foreign Keys and Check Constraints are not trusted <_.Name>" { + Context "Testing Foreign Keys and Check Constraints are not trusted <_.Name>" { It "Database <_.Database> Foreign Key <_.Name> on table <_.Parent> should be trusted on <_.SqlInstance>" -Skip:$skip -ForEach $psitem.Databases.Where{ if ($Database) { $_.Name -in $Database } else { $psitem.ConfigValues.fkcktrustedexclude -notcontains $psitem.Name } }.ForeignKeys { $psitem.IsChecked | Should -Be $true -Because "This can have a huge performance impact on queries. SQL Server won't use untrusted constraints to build better execution plans. It will also avoid data violation" @@ -319,3 +319,37 @@ Describe "Foreign keys and check constraints not trusted" -Tag FKCKTrusted, Low } } +Describe "Last Full Backup Times" -Tag LastFullBackup, LastBackup, Backup, DISA, Varied -ForEach $InstancesToTest { + $Skip = ($__dbcconfig | Where-Object Name -EQ 'skip.database.lastfullbackup').Value + #TODO: also skip secondaries? and read only? + Context "Testing last full backups on <_.Name>" { + It "Database <_.Name> should have full backups less than <_.ConfigValues.fullmaxdays> on <_.SqlInstance>" -Skip:$skip -ForEach $psitem.Databases.Where{ if ($Database) { $_.Name -in $Database } else { $psitem.ConfigValues.fullbackupexclude -notcontains $psitem.Name } }.Where{if ($psitem.ConfigValues.skipreadonly) { -not $psitem.readonly } else {$psitem} }.Where{if ($psitem.ConfigValues.skipsecondaries) { $psitem.AGReplicaRole -ne 'Secondary' } else {$psitem} } { #TODO: secondary? is that right + $psitem.LastFullBackup.ToUniversalTime() | Should -BeGreaterThan (Get-Date).ToUniversalTime().AddDays( - ($psitem.ConfigValues.fullmaxdays)) -Because "Taking regular backups is extraordinarily important" + } + } +} + + +#TODO: convert checks +# TestLastBackup +# TestLastBackupVerifyOnly +# LastGoodCheckDb +# IdentityUsage +# DuplicateIndex +# UnusedIndex +# DisabledIndex +# DatabaseGrowthEvent +# LastFullBackup +# LastDiffBackup +# LastLogBackup +# LogfilePercentUsed +# LogfileSize +# FutureFileGrowth +# FileGroupBalanced +# CertificateExpiration +# DatafileAutoGrowthType +# OrphanedUser +# MaxDopDatabase +# DatabaseExists +# CLRAssembliesSafe +# SymmetricKeyEncryptionLevel diff --git a/source/internal/configurations/configuration.ps1 b/source/internal/configurations/configuration.ps1 index 427385a3..81088f44 100644 --- a/source/internal/configurations/configuration.ps1 +++ b/source/internal/configurations/configuration.ps1 @@ -18,7 +18,7 @@ $EmailValidationSb = { } Register-PSFConfigValidation -Name validation.EmailValidation -ScriptBlock $EmailValidationSb -$__dbachecksNotv5 = 'ADUser', 'BuiltInAdmin', 'EngineServiceAdmin', 'FullTextServiceAdmin', 'LocalWindowsGroup', 'PublicPermission', 'SqlBrowserServiceAccount', 'TempDbConfiguration','CertificateExpiration', 'DatabaseExists', 'DatabaseGrowthEvent', 'DatafileAutoGrowthType', 'DisabledIndex', 'DuplicateIndex', 'FileGroupBalanced', 'FutureFileGrowth', 'IdentityUsage', 'LastDiffBackup', 'LastFullBackup', 'LastGoodCheckDb', 'LastLogBackup', 'LogfilePercentUsed', 'LogfileSize', 'MaxDopDatabase', 'OrphanedUser', 'SymmetricKeyEncryptionLevel', 'TestLastBackup', 'TestLastBackupVerifyOnly', 'UnusedIndex', 'PowerPlan', 'SPN', 'DiskCapacity', 'PingComputer', 'CPUPrioritisation', 'DiskAllocationUnit', 'NonStandardPort', 'ServerProtocol', 'OlaInstalled', 'SystemFull', 'UserFull', 'UserDiff', 'UserLog', 'CommandLog', 'SystemIntegrityCheck', 'UserIntegrityCheck', 'UserIndexOptimize', 'OutputFileCleanup', 'DeleteBackupHistory', 'PurgeJobHistory', 'DomainName', 'OrganizationalUnit', 'ClusterHealth', 'LogShippingPrimary', 'LogShippingSecondary' +$__dbachecksNotv5 = 'ADUser', 'BuiltInAdmin', 'EngineServiceAdmin', 'FullTextServiceAdmin', 'LocalWindowsGroup', 'PublicPermission', 'SqlBrowserServiceAccount', 'TempDbConfiguration','CertificateExpiration', 'DatabaseExists', 'DatabaseGrowthEvent', 'DatafileAutoGrowthType', 'DisabledIndex', 'DuplicateIndex', 'FileGroupBalanced', 'FutureFileGrowth', 'IdentityUsage', 'LastDiffBackup', 'LastGoodCheckDb', 'LastLogBackup', 'LogfilePercentUsed', 'LogfileSize', 'MaxDopDatabase', 'OrphanedUser', 'SymmetricKeyEncryptionLevel', 'TestLastBackup', 'TestLastBackupVerifyOnly', 'UnusedIndex', 'PowerPlan', 'SPN', 'DiskCapacity', 'PingComputer', 'CPUPrioritisation', 'DiskAllocationUnit', 'NonStandardPort', 'ServerProtocol', 'OlaInstalled', 'SystemFull', 'UserFull', 'UserDiff', 'UserLog', 'CommandLog', 'SystemIntegrityCheck', 'UserIntegrityCheck', 'UserIndexOptimize', 'OutputFileCleanup', 'DeleteBackupHistory', 'PurgeJobHistory', 'DomainName', 'OrganizationalUnit', 'ClusterHealth', 'LogShippingPrimary', 'LogShippingSecondary' Set-PSFConfig -Module dbachecks -Name checks.notv5ready -Value @($__dbachecksNotv5) -Initialize -Description "Checks that have not been converted to v5 yet" @@ -178,7 +178,9 @@ Set-PSFConfig -Module dbachecks -Name policy.database.clrassembliessafeexcludedb Set-PSFConfig -Module dbachecks -Name policy.database.pseudosimpleexcludedb -Value @('tempdb', 'model') -Initialize -Description "A List of databases that we do not want to check for pseudosimple recovery modelasd a" Set-PSFConfig -Module dbachecks -Name policy.database.contdbautocloseexclude -Value @('msdb') -Initialize -Description "A List of contained database that we we do not want to check for autoclose" Set-PSFConfig -Module dbachecks -Name policy.database.contdbsqlauthexclude -Value @() -Initialize -Description "A list of databases that we do not want to check for contained databases with SQL authenticated users" -Set-PSFConfig -Module dbachecks -Name policy.database.logfilepercentused -Value 75 -Initialize -Description " The % log used we should stay below" +Set-PSFConfig -Module dbachecks -Name policy.database.logfilepercentused -Value 75 -Initialize -Description "The % log used we should stay below" +Set-PSFConfig -Module dbachecks -Name policy.database.fullbackupexclude -Value @('tempdb') -Initialize -Description "The list of databases that we do not want to check the date of the last full backup of." + # Policy for Ola Hallengren Maintenance Solution Set-PSFConfig -Module dbachecks -Name policy.ola.installed -Validation bool -Value $true -Initialize -Description "Checks to see if Ola Hallengren solution is installed" @@ -344,6 +346,8 @@ Set-PSFConfig -Module dbachecks -Name skip.database.recoverymodel -Validation bo Set-PSFConfig -Module dbachecks -Name skip.database.pseudosimple -Validation bool -Value $false -Initialize -Description "Skip the database PseudoSimple recovery model test" Set-PSFConfig -Module dbachecks -Name skip.database.pageverify -Validation bool -Value $false -Initialize -Description "Skip the database page verify test" Set-PSFConfig -Module dbachecks -Name skip.database.fkcktrusted -Validation bool -Value $false -Initialize -Description "Skip the check for foreign keys and constraints being trusted" +Set-PSFConfig -Module dbachecks -Name skip.database.lastfullbackup -Validation bool -Value $false -Initialize -Description "Skip the check for last full backup" + Set-PSFConfig -Module dbachecks -Name skip.logshiptesting -Validation bool -Value $false -Initialize -Description "Skip the logshipping test" diff --git a/source/internal/functions/Get-AllDatabaseInfo.ps1 b/source/internal/functions/Get-AllDatabaseInfo.ps1 index 04748e78..5a304ba7 100644 --- a/source/internal/functions/Get-AllDatabaseInfo.ps1 +++ b/source/internal/functions/Get-AllDatabaseInfo.ps1 @@ -158,13 +158,21 @@ function Get-AllDatabaseInfo { } 'PageVerify' { $pageverify = $true - $ConfigValues | Add-Member -MemberType NoteProperty -Name 'pageverifyexclude' -Value ($__dbcconfig | Where-Object Name -EQ 'policy.database.contdbsqlauthexclude').Value + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'pageverifyexclude' -Value ($__dbcconfig | Where-Object Name -EQ 'policy.database.contdbsqlauthexclude').Value #TODO: fix that config name $ConfigValues | Add-Member -MemberType NoteProperty -Name 'pageverify' -Value ($__dbcconfig | Where-Object Name -EQ 'policy.pageverify').Value } 'FKCKTrusted' { $trusted = $true $ConfigValues | Add-Member -MemberType NoteProperty -Name 'fkcktrustedexclude' -Value ($__dbcconfig | Where-Object Name -EQ 'policy.database.fkcktrustedexclude').Value } + 'LastFullBackup' { + $lastFullBackup = $true + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'fullbackupexclude' -Value ($__dbcconfig | Where-Object Name -EQ 'policy.database.fullbackupexclude').Value + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'fullmaxdays' -Value ($__dbcconfig | Where-Object Name -EQ 'policy.backup.fullmaxdays').Value + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'graceperiod' -Value ($__dbcconfig | Where-Object Name -EQ 'policy.backup.newdbgraceperiod').Value + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'skipreadonly' -Value ($__dbcconfig | Where-Object Name -EQ 'skip.backup.readonly').Value + $ConfigValues | Add-Member -MemberType NoteProperty -Name 'skipsecondaries' -Value ($__dbcconfig | Where-Object Name -EQ 'skip.backup.secondaries').Value + } Default { } } @@ -195,7 +203,7 @@ function Get-AllDatabaseInfo { Trustworthy = @(if ($trustworthy) { $psitem.Trustworthy }) Status = @(if ($status) { $psitem.Status }) IsDatabaseSnapshot = @(if ($status) { $psitem.IsDatabaseSnapshot }) # needed for status test - Readonly = @(if ($status) { $psitem.Readonly }) # needed for status test + Readonly = @(if ($status -or $lastFullBackup) { $psitem.Readonly }) # needed for status test & lastfullbackup QueryStore = @(if ($qs) { $psitem.QueryStoreOptions.ActualState }) CompatibilityLevel = @(if ($compatibilitylevel) { $psitem.CompatibilityLevel }) ServerLevel = @(if ($compatibilitylevel) { [Enum]::GetNames('Microsoft.SqlServer.Management.Smo.CompatibilityLevel').Where{ $psitem -match $Instance.VersionMajor } }) @@ -208,6 +216,8 @@ function Get-AllDatabaseInfo { PageVerify = @(if ($pageverify) { $psitem.PageVerify }) ForeignKeys = @(if ($trusted) {$psitem.Tables.ForeignKeys | Where-Object {-not $_.NotForReplication} | Select-Object Name, Parent, @{l='Database';e={$_.Parent.Parent.Name}}, IsChecked } ) Constraints = @(if ($trusted) {$psitem.Tables.Checks | Where-Object {(-not $_.NotForReplication) -and $_.IsEnabled} | Select-Object Name, Parent, @{l='Database';e={$_.Parent.Parent.Name}}, IsChecked } ) + LastFullBackup = @(if ($lastFullBackup) {$psitem.LastBackupDate}) + AGReplicaRole = @(if ($lastFullBackup) { if ($psitem.AvailabilityGroupName) {$psitem.parent.AvailabilityGroups[$psitem.AvailabilityGroupName].LocalReplicaRole}}) # TODO: this needs testing! } } } From 5ca1e115e949d84fe73fd2af2f2fcd7592421138 Mon Sep 17 00:00:00 2001 From: jpomfret Date: Mon, 24 Mar 2025 19:08:05 +0000 Subject: [PATCH 2/3] forgot the changelog again.. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bc33735..818b5055 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - fixed typo in Invoke-PerfAndValidateCheck so we know which version we're looking at +- New database level check `LastFullBackup` ## [3.0.2] - 2025-03-10 From 800879a26511e10cc1b5d4334e9b2f74d60d610e Mon Sep 17 00:00:00 2001 From: jpomfret Date: Fri, 18 Apr 2025 12:30:18 +0000 Subject: [PATCH 3/3] working... --- source/checks/Databasev5.Tests.ps1 | 3 ++- source/internal/functions/Get-AllDatabaseInfo.ps1 | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/source/checks/Databasev5.Tests.ps1 b/source/checks/Databasev5.Tests.ps1 index 00f13e69..ec9f84e1 100644 --- a/source/checks/Databasev5.Tests.ps1 +++ b/source/checks/Databasev5.Tests.ps1 @@ -319,11 +319,12 @@ Describe "Foreign keys and check constraints not trusted" -Tag FKCKTrusted, Low } } +#TODO: test if database is offline or not `IsAccessible` Describe "Last Full Backup Times" -Tag LastFullBackup, LastBackup, Backup, DISA, Varied -ForEach $InstancesToTest { $Skip = ($__dbcconfig | Where-Object Name -EQ 'skip.database.lastfullbackup').Value #TODO: also skip secondaries? and read only? Context "Testing last full backups on <_.Name>" { - It "Database <_.Name> should have full backups less than <_.ConfigValues.fullmaxdays> on <_.SqlInstance>" -Skip:$skip -ForEach $psitem.Databases.Where{ if ($Database) { $_.Name -in $Database } else { $psitem.ConfigValues.fullbackupexclude -notcontains $psitem.Name } }.Where{if ($psitem.ConfigValues.skipreadonly) { -not $psitem.readonly } else {$psitem} }.Where{if ($psitem.ConfigValues.skipsecondaries) { $psitem.AGReplicaRole -ne 'Secondary' } else {$psitem} } { #TODO: secondary? is that right + It "Database <_.Name> should have full backups less than <_.ConfigValues.fullmaxdays> on <_.SqlInstance>" -Skip:$skip -ForEach $psitem.Databases.Where{if ($Database) { $_.Name -in $Database } else { $psitem.ConfigValues.fullbackupexclude -notcontains $psitem.Name } }.Where{if ($psitem.ConfigValues.skipreadonly) { -not $psitem.readonly } else {$psitem} }.Where{if ($psitem.ConfigValues.skipsecondaries) { $psitem.AGReplicaRole -ne 'Secondary' } else {$psitem} } { #TODO: secondary? is that right $psitem.LastFullBackup.ToUniversalTime() | Should -BeGreaterThan (Get-Date).ToUniversalTime().AddDays( - ($psitem.ConfigValues.fullmaxdays)) -Because "Taking regular backups is extraordinarily important" } } diff --git a/source/internal/functions/Get-AllDatabaseInfo.ps1 b/source/internal/functions/Get-AllDatabaseInfo.ps1 index 5a304ba7..3cd33235 100644 --- a/source/internal/functions/Get-AllDatabaseInfo.ps1 +++ b/source/internal/functions/Get-AllDatabaseInfo.ps1 @@ -217,7 +217,7 @@ function Get-AllDatabaseInfo { ForeignKeys = @(if ($trusted) {$psitem.Tables.ForeignKeys | Where-Object {-not $_.NotForReplication} | Select-Object Name, Parent, @{l='Database';e={$_.Parent.Parent.Name}}, IsChecked } ) Constraints = @(if ($trusted) {$psitem.Tables.Checks | Where-Object {(-not $_.NotForReplication) -and $_.IsEnabled} | Select-Object Name, Parent, @{l='Database';e={$_.Parent.Parent.Name}}, IsChecked } ) LastFullBackup = @(if ($lastFullBackup) {$psitem.LastBackupDate}) - AGReplicaRole = @(if ($lastFullBackup) { if ($psitem.AvailabilityGroupName) {$psitem.parent.AvailabilityGroups[$psitem.AvailabilityGroupName].LocalReplicaRole}}) # TODO: this needs testing! + AGReplicaRole = @(if ($lastFullBackup) { if ($psitem.AvailabilityGroupName) {$psitem.parent.AvailabilityGroups[$psitem.AvailabilityGroupName].LocalReplicaRole} else { 'N\A' }}) # TODO: this needs testing! } } }