Skip to content

Commit

Permalink
feat(new): Added Azure.SQL.MaintenanceWindow (Azure#2978)
Browse files Browse the repository at this point in the history
* feat(new): Added Azure.SQL.MaintenanceWindow

* Update src/PSRule.Rules.Azure/rules/Azure.SQL.Rule.ps1

Co-authored-by: Bernie White <[email protected]>

* Update src/PSRule.Rules.Azure/rules/Azure.SQL.Rule.ps1

Co-authored-by: Bernie White <[email protected]>

* Update src/PSRule.Rules.Azure/rules/Azure.SQL.Rule.ps1

Co-authored-by: Bernie White <[email protected]>

* Update src/PSRule.Rules.Azure/rules/Azure.SQL.Rule.ps1

Co-authored-by: Bernie White <[email protected]>

* Update src/PSRule.Rules.Azure/rules/Azure.SQL.Rule.ps1

Co-authored-by: Bernie White <[email protected]>

* Update docs/en/rules/Azure.SQL.MaintenanceWindow.md

Co-authored-by: Bernie White <[email protected]>

* feat: Added logic for SKUs and master database

* fix: Hardcode location for readability

---------

Co-authored-by: Bernie White <[email protected]>
  • Loading branch information
BenjaminEngeset and BernieWhite authored Jul 9, 2024
1 parent 783a85f commit fdefbf6
Show file tree
Hide file tree
Showing 6 changed files with 459 additions and 6 deletions.
5 changes: 5 additions & 0 deletions docs/CHANGELOG-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers

## Unreleased

- New rules:
- Azure SQL Database:
- Verify that Azure SQL databases have a customer-controlled maintenance window configured by @BenjaminEngeset.
[#2956](https://github.com/Azure/PSRule.Rules.Azure/issues/2956)

## v1.38.0

What's changed since v1.37.0:
Expand Down
99 changes: 99 additions & 0 deletions docs/en/rules/Azure.SQL.MaintenanceWindow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
severity: Important
pillar: Reliability
category: RE:04 Target metrics
resource: Azure Database
online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.SQL.MaintenanceWindow/
---

# Customer-controlled maintenance window configuration

## SYNOPSIS

Configure a customer-controlled maintenance window for Azure SQL databases.

## DESCRIPTION

Azure SQL databases undergo periodic maintenance to ensure your managed database remains secure, stable, and up-to-date. This maintenance includes applying security updates, system upgrades, and software patches.

Maintenance windows can be scheduled in two ways for each database:

- System-Managed Schedule: The system automatically selects a 9-hour maintenance window between 8:00 AM to 5:00 PM local time, Monday - Sunday.
- Urgent updates may occur outside of it. To ensure all updates occur only during the maintenance window, select a non-default option.
- Custom Schedule: You can specify a preferred 8-hour maintenance window by choosing between two non-default maintenance windows:
- Weekday window: 10:00 PM to 6:00 AM local time, Monday - Thursday.
- Weekend window: 10:00 PM to 6:00 AM local time, Friday - Sunday.

By configuring a customer-controlled maintenance window, you can schedule updates to occur during a preferred time, ideally outside business hours, minimizing disruptions.

There are limitations to the non-default maintenance windows. You can find more details about this in the documentation.

## RECOMMENDATION

Consider using a customer-controlled maintenance window to efficiently schedule updates and minimize disruptions.

## EXAMPLES

### Configure with Azure template

To configure databases that pass this rule:

- Set the `properties.maintenanceConfigurationId` property to `/subscriptions/<subscriptionId>/providers/Microsoft.Maintenance/publicMaintenanceConfigurations/SQL_<RegionPlaceholder>_DB_1` or `SQL_<RegionPlaceholder>_DB_2`.

For example:

```json
{
"type": "Microsoft.Sql/servers/databases",
"apiVersion": "2023-05-01-preview",
"name": "[format('{0}/{1}', parameters('serverName'), parameters('sqlDbName'))]",
"location": "westeurope",
"sku": {
"name": "P1",
"tier": "Premium"
},
"properties": {
"maintenanceConfigurationId": "[subscriptionResourceId('Microsoft.Maintenance/publicMaintenanceConfigurations', 'SQL_WestEurope_DB_1')]"
},
"dependsOn": [
"[resourceId('Microsoft.Sql/servers', parameters('serverName'))]"
]
}
```

### Configure with Bicep

To configure databases that pass this rule:

- Set the `properties.maintenanceConfigurationId` property to `/subscriptions/<subscriptionId>/providers/Microsoft.Maintenance/publicMaintenanceConfigurations/SQL_<RegionPlaceholder>_DB_1` or `SQL_<RegionPlaceholder>_DB_2`.

For example:

```bicep
resource maintenanceWindow 'Microsoft.Maintenance/publicMaintenanceConfigurations@2023-04-01' existing = {
scope: subscription()
name: 'SQL_WestEurope_DB_1'
}
resource sqlDb 'Microsoft.Sql/servers/databases@2023-05-01-preview' = {
parent: sqlServer
name: sqlDbName
location: 'westeurope'
sku: {
name: 'P1'
tier: 'Premium'
}
properties: {
maintenanceConfigurationId: maintenanceWindow.id
}
}
```

## LINKS

- [RE:04 Target metrics](https://learn.microsoft.com/azure/well-architected/reliability/metrics)
- [Maintenance window in Azure SQL Database](https://learn.microsoft.com/azure/azure-sql/database/maintenance-window)
- [Configure maintenance window](https://learn.microsoft.com/azure/azure-sql/database/maintenance-window-configure)
- [Azure deployment reference - Maintenance Configuration](https://learn.microsoft.com/azure/templates/microsoft.maintenance/publicmaintenanceconfigurations)
- [Azure deployment reference - Azure SQL Database](https://learn.microsoft.com/azure/templates/microsoft.sql/servers/databases)
- [Azure deployment reference - Elastic Pool](https://learn.microsoft.com/azure/templates/microsoft.sql/servers/elasticpools)
1 change: 1 addition & 0 deletions src/PSRule.Rules.Azure/en/PSRule-rules.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,5 @@
ResStorageSensitiveDataThreatDetection = "The storage account '{0}' should have sensitive data threat detection in Microsoft Defender for Storage configured."
ResAPIDefender = "The API '{0}' should be onboarded to Microsoft Defender for APIs."
InsecureParameterType = "The parameter '{0}' with type '{1}' is not secure."
AzureSQLDatabaseMaintenanceWindow = "The {0} ({1}) should have a customer-controlled maintenance window configured."
}
61 changes: 55 additions & 6 deletions src/PSRule.Rules.Azure/rules/Azure.SQL.Rule.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,25 @@
Rule 'Azure.SQL.FirewallRuleCount' -Ref 'AZR-000183' -Type 'Microsoft.Sql/servers' -Tag @{ release = 'GA'; ruleSet = '2020_06'; 'Azure.WAF/pillar' = 'Security'; } {
$firewallRules = @(GetSubResources -ResourceType 'Microsoft.Sql/servers/firewallRules');
$Assert.
LessOrEqual($firewallRules, '.', 10).
WithReason(($LocalizedData.ExceededFirewallRuleCount -f $firewallRules.Length, 10), $True);
LessOrEqual($firewallRules, '.', 10).
WithReason(($LocalizedData.ExceededFirewallRuleCount -f $firewallRules.Length, 10), $True);
}

# Synopsis: Determine if access from Azure services is required
Rule 'Azure.SQL.AllowAzureAccess' -Ref 'AZR-000184' -Type 'Microsoft.Sql/servers' -Tag @{ release = 'GA'; ruleSet = '2020_06'; 'Azure.WAF/pillar' = 'Security'; } {
$firewallRules = @(GetSubResources -ResourceType 'Microsoft.Sql/servers/firewallRules' | Where-Object {
$_.ResourceName -eq 'AllowAllWindowsAzureIps' -or
$_.ResourceName -eq 'AllowAllWindowsAzureIps' -or
($_.properties.StartIpAddress -eq '0.0.0.0' -and $_.properties.EndIpAddress -eq '0.0.0.0')
})
})
$firewallRules.Length -eq 0;
}

# Synopsis: Determine if there is an excessive number of permitted IP addresses
Rule 'Azure.SQL.FirewallIPRange' -Ref 'AZR-000185' -Type 'Microsoft.Sql/servers' -Tag @{ release = 'GA'; ruleSet = '2020_06'; 'Azure.WAF/pillar' = 'Security'; } {
$summary = GetIPAddressSummary
$Assert.
LessOrEqual($summary, 'Public', 10).
WithReason(($LocalizedData.DBServerFirewallPublicIPRange -f $summary.Public, 10), $True);
LessOrEqual($summary, 'Public', 10).
WithReason(($LocalizedData.DBServerFirewallPublicIPRange -f $summary.Public, 10), $True);
}

# Synopsis: Enable Microsoft Defender for Cloud for Azure SQL logical server
Expand Down Expand Up @@ -163,6 +163,55 @@ Rule 'Azure.SQL.FGName' -Ref 'AZR-000193' -Type 'Microsoft.Sql/servers/failoverG

#endregion Failover group

#region Maintenance window

# Synopsis: Configure a customer-controlled maintenance window for Azure SQL databases.
Rule 'Azure.SQL.MaintenanceWindow' -Ref 'AZR-000440' -Type 'Microsoft.Sql/servers', 'Microsoft.Sql/servers/databases', 'Microsoft.Sql/servers/elasticPools' -Tag @{ release = 'GA'; ruleSet = '2024_09'; 'Azure.WAF/pillar' = 'Reliability'; } {
$notSupportedSkus = '^(?:GP_Fsv2|HS_DC)_|B|S0|S1'
if ($PSRule.TargetType -eq 'Microsoft.Sql/servers') {
# Microsoft.Sql/servers/elasticPools maintenance configuration is inherited to all databases within the pool, so we only need to check databases that is not in a pool.
$resources = @(GetSubResources -ResourceType 'Microsoft.Sql/servers/databases', 'Microsoft.Sql/servers/elasticPools' |
Where-Object { ($_.sku.name -notmatch $notSupportedSkus) -or (-not $_.properties.psobject.Properties['elasticPoolId']) -or ($_.name -notlike '*/master' -or $_.name -eq 'master') })

if ($resources.Count -eq 0) {
return $Assert.Pass()
}

foreach ($resource in $resources) {
$Assert.Match($resource, 'properties.maintenanceConfigurationId', '\/publicMaintenanceConfigurations\/SQL_[A-Za-z]+[A-Za-z0-9]*_DB_[12]$', $False).
Reason(
$LocalizedData.AzureSQLDatabaseMaintenanceWindow,
$(if ($resource.type -eq 'Microsoft.Sql/servers/databases') { 'database' } else { 'elastic pool' }),
$resource.Name
).PathPrefix('resources')
}
}

elseif ($PSRule.TargetType -eq 'Microsoft.Sql/servers/databases') {
if (($TargetObject.sku.name -match $notSupportedSkus) -or $TargetObject.properties.psobject.Properties['elasticPoolId'] -or ($PSRule.TargetName -like '*/master' -or $_.name -eq 'master')) {
return $Assert.Pass()
}
$Assert.Match($TargetObject, 'properties.maintenanceConfigurationId', '\/publicMaintenanceConfigurations\/SQL_[A-Za-z]+[A-Za-z0-9]*_DB_[12]$', $False).
Reason(
$LocalizedData.AzureSQLDatabaseMaintenanceWindow,
'database',
$TargetObject.Name
)
}

# Microsoft.Sql/servers/elasticPools maintenance configuration is inherited to all databases within the pool.
else {
$Assert.Match($TargetObject, 'properties.maintenanceConfigurationId', '\/publicMaintenanceConfigurations\/SQL_[A-Za-z]+[A-Za-z0-9]*_DB_[12]$', $False).
Reason(
$LocalizedData.AzureSQLDatabaseMaintenanceWindow,
'elastic pool',
$TargetObject.Name
)
}
}

#endregion Maintenance window

#region Helper functions

function global:IsMasterDatabase {
Expand Down
26 changes: 26 additions & 0 deletions tests/PSRule.Rules.Azure.Tests/Azure.SQL.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,32 @@ Describe 'Azure.SQL' -Tag 'SQL', 'SQLDB' {
$ruleResult.Length | Should -Be 3;
$ruleResult.TargetName | Should -BeIn 'server-B', 'server-D', 'AzureADOnlyAuthentication-B';
}

It 'Azure.SQL.MaintenanceWindow' {
$filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.SQL.MaintenanceWindow' };

# Fail
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' });
$ruleResult.Length | Should -Be 5;
$ruleResult.TargetName | Should -BeIn 'server-B', 'server-C', 'server-A/database-A', 'server-E/pool-A', 'server-F/pool-A';

$ruleResult[0].Reason | Should -BeExactly @(
"The database (database-A) should have a customer-controlled maintenance window configured.";
"The elastic pool (pool-A) should have a customer-controlled maintenance window configured.";
)
$ruleResult[1].Reason | Should -BeExactly @(
"The database (database-A) should have a customer-controlled maintenance window configured.";
"The elastic pool (pool-A) should have a customer-controlled maintenance window configured.";
)
$ruleResult[2].Reason | Should -BeExactly "The database (database-A) should have a customer-controlled maintenance window configured.";
$ruleResult[3].Reason | Should -BeExactly "The elastic pool (server-E/pool-A) should have a customer-controlled maintenance window configured.";
$ruleResult[4].Reason | Should -BeExactly "The elastic pool (server-F/pool-A) should have a customer-controlled maintenance window configured.";

# Pass
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
$ruleResult.Length | Should -Be 5;
$ruleResult.TargetName | Should -BeIn 'server-A', 'server-D', 'server-A/master', 'server-A/database-B', 'server-G/pool-A';
}
}

Context 'Resource name - Azure.SQL.ServerName' {
Expand Down
Loading

0 comments on commit fdefbf6

Please sign in to comment.