From eab4d2d1498cc2aaf44f7ff49706ea944c34dcf7 Mon Sep 17 00:00:00 2001 From: Thomas Defise <36169753+tdefise@users.noreply.github.com> Date: Sun, 22 Oct 2023 13:13:45 +0200 Subject: [PATCH] feat(terraform): Ensure that the SQL database is zone-redundant (#5540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added the check * Removed useless space * Fixed flake8 findings * adjust check ID --------- Co-authored-by: Thomas Defise Co-authored-by: Anton GrĂ¼bel --- .../azure/SQLDatabaseZoneRedundant.py | 43 ++++++++++++++++++ .../example_SQLDatabaseZoneRedundant/main.tf | 44 +++++++++++++++++++ .../azure/test_SQLDatabaseZoneRedundant.py | 42 ++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 checkov/terraform/checks/resource/azure/SQLDatabaseZoneRedundant.py create mode 100644 tests/terraform/checks/resource/azure/example_SQLDatabaseZoneRedundant/main.tf create mode 100644 tests/terraform/checks/resource/azure/test_SQLDatabaseZoneRedundant.py diff --git a/checkov/terraform/checks/resource/azure/SQLDatabaseZoneRedundant.py b/checkov/terraform/checks/resource/azure/SQLDatabaseZoneRedundant.py new file mode 100644 index 00000000000..d3d59c623af --- /dev/null +++ b/checkov/terraform/checks/resource/azure/SQLDatabaseZoneRedundant.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from checkov.common.models.enums import CheckCategories +from checkov.terraform.checks.resource.base_resource_value_check import BaseResourceValueCheck + + +class SQLDatabaseZoneRedundant(BaseResourceValueCheck): + def __init__(self) -> None: + """ + This is a best practise which helps to: + - Improved High Availability: Zone redundancy ensures that your database is replicated + across Availability Zones within an Azure region. If one Availability Zone experiences an outage, + your database continues to operate from the other zones, minimizing downtime. + - Reduced Maintenance Downtime: Zone-redundant configurations often require + less planned maintenance downtime because updates and patches can be applied to + one zone at a time while the other zones continue to serve traffic. + - Improved Scalability: Zone-redundant configurations are designed to scale with your workload. + You can take advantage of features like Hyperscale to dynamically adjust resources based on + your database's performance needs. + - Improved SLA: Azure SQL Database zone-redundant configurations typically offer + a higher service-level agreement (SLA) for availability compared to non-zone-redundant configurations. + + However, it's critical to note that: + Note that: + - Zone-redundant availability is available to databases in the + General Purpose, Premium, Business Critical and Hyperscale service tiers of the vCore purchasing model, + and not the Basic and Standard service tiers of the DTU-based purchasing model. + - This may not be required for: + - Databases that supports applications which doesn't a high maturity in terms of "High Availability" + - Databases that are very sensitive to network latency that may increase the transaction commit time, + and thus impact the performance of some OLTP workloads. + """ + name = "Ensure the Azure SQL Database Namespace is zone redundant" + id = "CKV_AZURE_229" + supported_resources = ("azurerm_mssql_database",) + categories = (CheckCategories.BACKUP_AND_RECOVERY,) + super().__init__(name=name, id=id, categories=categories, supported_resources=supported_resources) + + def get_inspected_key(self) -> str: + return "zone_redundant" + + +check = SQLDatabaseZoneRedundant() diff --git a/tests/terraform/checks/resource/azure/example_SQLDatabaseZoneRedundant/main.tf b/tests/terraform/checks/resource/azure/example_SQLDatabaseZoneRedundant/main.tf new file mode 100644 index 00000000000..5eae00a9627 --- /dev/null +++ b/tests/terraform/checks/resource/azure/example_SQLDatabaseZoneRedundant/main.tf @@ -0,0 +1,44 @@ +resource "azurerm_mssql_database" "pass" { + name = "example-database" + server_id = azurerm_mssql_server.example.id + collation = "SQL_Latin1_General_CP1_CI_AS" + license_type = "LicenseIncluded" + max_size_gb = 4 + read_scale = true + sku_name = "S0" + zone_redundant = true + + tags = { + environment = "Production" + } +} + +resource "azurerm_mssql_database" "fail2" { + name = "example-database" + server_id = azurerm_mssql_server.example.id + collation = "SQL_Latin1_General_CP1_CI_AS" + license_type = "LicenseIncluded" + max_size_gb = 4 + read_scale = true + sku_name = "S0" + zone_redundant = false + + tags = { + environment = "Production" + } +} + +resource "azurerm_mssql_database" "fail" { + name = "example-database" + server_id = azurerm_mssql_server.example.id + collation = "SQL_Latin1_General_CP1_CI_AS" + license_type = "LicenseIncluded" + max_size_gb = 4 + read_scale = true + sku_name = "S0" + + tags = { + environment = "Production" + } + +} \ No newline at end of file diff --git a/tests/terraform/checks/resource/azure/test_SQLDatabaseZoneRedundant.py b/tests/terraform/checks/resource/azure/test_SQLDatabaseZoneRedundant.py new file mode 100644 index 00000000000..b06aba11d04 --- /dev/null +++ b/tests/terraform/checks/resource/azure/test_SQLDatabaseZoneRedundant.py @@ -0,0 +1,42 @@ +import os +import unittest + +from checkov.runner_filter import RunnerFilter +from checkov.terraform.runner import Runner +from checkov.terraform.checks.resource.azure.SQLDatabaseZoneRedundant import check + + +class TestSQLDatabaseZoneRedundant (unittest.TestCase): + + def test(self): + runner = Runner() + current_dir = os.path.dirname(os.path.realpath(__file__)) + + test_files_dir = os.path.join(current_dir, "example_SQLDatabaseZoneRedundant") + report = runner.run(root_folder=test_files_dir, + runner_filter=RunnerFilter(checks=[check.id])) + summary = report.get_summary() + + passing_resources = { + 'azurerm_mssql_database.pass', + } + failing_resources = { + 'azurerm_mssql_database.fail', + 'azurerm_mssql_database.fail2', + } + skipped_resources = {} + + passed_check_resources = set([c.resource for c in report.passed_checks]) + failed_check_resources = set([c.resource for c in report.failed_checks]) + + self.assertEqual(summary['passed'], len(passing_resources)) + self.assertEqual(summary['failed'], len(failing_resources)) + self.assertEqual(summary['skipped'], len(skipped_resources)) + self.assertEqual(summary['parsing_errors'], 0) + + self.assertEqual(passing_resources, passed_check_resources) + self.assertEqual(failing_resources, failed_check_resources) + + +if __name__ == '__main__': + unittest.main()