From 151dcd28954ea606196244ca2680d2abf2fcea20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Fri, 19 Jun 2026 09:07:54 +0200 Subject: [PATCH 1/5] feat(compliance): add DORA compliance framework for GCP (#11642) --- prowler/CHANGELOG.md | 2 + prowler/compliance/dora_2022_2554.json | 220 +++++++++++++++++++++++++ 2 files changed, 222 insertions(+) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index c9bfa7a1c7..197a7f961d 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -31,6 +31,8 @@ All notable changes to the **Prowler SDK** are documented in this file. - `entra_directory_sync_object_takeover_blocked` check for the M365 provider, verifying that hybrid Entra tenants block cloud object takeover through both soft-match and hard-match directory synchronization [(#11098)](https://github.com/prowler-cloud/prowler/pull/11098) - `entra_conditional_access_policy_no_deleted_object_references` check for M365 provider [(#11236)](https://github.com/prowler-cloud/prowler/pull/11236) - `aks_cluster_defender_enabled` check for Azure provider, verifying that AKS clusters have Microsoft Defender security monitoring enabled [(#11028)](https://github.com/prowler-cloud/prowler/pull/11028) +- DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the GCP provider, mapping existing GCP checks across the five DORA pillars [(#11642)](https://github.com/prowler-cloud/prowler/pull/11642) + ### 🔄 Changed diff --git a/prowler/compliance/dora_2022_2554.json b/prowler/compliance/dora_2022_2554.json index 1bc4ed41da..4a1db41beb 100644 --- a/prowler/compliance/dora_2022_2554.json +++ b/prowler/compliance/dora_2022_2554.json @@ -130,6 +130,20 @@ "iam_subscription_roles_owner_custom_not_created", "iam_role_user_access_admin_restricted", "iam_custom_role_has_permissions_to_administer_resource_locks" + ], + "gcp": [ + "compute_project_os_login_enabled", + "compute_project_os_login_2fa_enabled", + "iam_no_service_roles_at_project_level", + "iam_sa_no_administrative_privileges", + "iam_role_kms_enforce_separation_of_duties", + "iam_role_sa_enforce_separation_of_duties", + "iam_organization_essential_contacts_configured", + "iam_account_access_approval_enabled", + "iam_service_account_unused", + "compute_instance_default_service_account_in_use", + "compute_instance_default_service_account_in_use_with_full_api_access", + "gke_cluster_no_default_service_account" ] } }, @@ -170,6 +184,14 @@ "defender_ensure_wdatp_is_enabled", "defender_auto_provisioning_log_analytics_agent_vms_on", "policy_ensure_asc_enforcement_enabled" + ], + "gcp": [ + "iam_cloud_asset_inventory_enabled", + "iam_audit_logs_enabled", + "logging_sink_created", + "gcr_container_scanning_enabled", + "artifacts_container_analysis_enabled", + "iam_organization_essential_contacts_configured" ] } }, @@ -217,6 +239,22 @@ "keyvault_key_rotation_enabled", "storage_key_rotation_90_days", "aks_network_policy_enabled" + ], + "gcp": [ + "cloudsql_instance_ssl_connections", + "compute_instance_encryption_with_csek_enabled", + "compute_instance_confidential_computing_enabled", + "bigquery_dataset_cmk_encryption", + "bigquery_table_cmk_encryption", + "dataproc_encrypted_with_cmks_disabled", + "kms_key_rotation_enabled", + "kms_key_rotation_max_90_days", + "dns_dnssec_disabled", + "dns_rsasha1_in_use_to_key_sign_in_dnssec", + "dns_rsasha1_in_use_to_zone_sign_in_dnssec", + "compute_network_not_legacy", + "compute_network_default_in_use", + "compute_instance_single_network_interface" ] } }, @@ -245,6 +283,14 @@ "network_watcher_enabled", "network_public_ip_shodan", "vm_scaleset_not_empty" + ], + "gcp": [ + "iam_cloud_asset_inventory_enabled", + "iam_service_account_unused", + "iam_sa_user_managed_key_unused", + "apikeys_key_exists", + "compute_instance_suspended_without_persistent_disks", + "compute_public_address_shodan" ] } }, @@ -329,6 +375,45 @@ "network_http_internet_access_restricted", "network_udp_internet_access_restricted", "network_bastion_host_exists" + ], + "gcp": [ + "kms_key_not_publicly_accessible", + "bigquery_dataset_public_access", + "bigquery_dataset_cmk_encryption", + "bigquery_table_cmk_encryption", + "cloudstorage_bucket_public_access", + "cloudstorage_bucket_uniform_bucket_level_access", + "cloudstorage_uses_vpc_service_controls", + "cloudsql_instance_public_access", + "cloudsql_instance_public_ip", + "cloudsql_instance_private_ip_assignment", + "cloudsql_instance_ssl_connections", + "cloudsql_instance_cmek_encryption_enabled", + "compute_firewall_ssh_access_from_the_internet_allowed", + "compute_firewall_rdp_access_from_the_internet_allowed", + "compute_instance_public_ip", + "compute_instance_shielded_vm_enabled", + "compute_instance_confidential_computing_enabled", + "compute_instance_serial_ports_in_use", + "compute_instance_block_project_wide_ssh_keys_disabled", + "compute_instance_ip_forwarding_is_enabled", + "compute_instance_encryption_with_csek_enabled", + "compute_image_not_publicly_shared", + "dataproc_encrypted_with_cmks_disabled", + "kms_key_rotation_enabled", + "gke_cluster_no_default_service_account", + "cloudfunction_function_inside_vpc", + "apikeys_api_restrictions_configured", + "apikeys_api_restricted_with_gemini_api", + "cloudsql_instance_mysql_local_infile_flag", + "cloudsql_instance_mysql_skip_show_database_flag", + "cloudsql_instance_sqlserver_contained_database_authentication_flag", + "cloudsql_instance_sqlserver_cross_db_ownership_chaining_flag", + "cloudsql_instance_sqlserver_external_scripts_enabled_flag", + "cloudsql_instance_sqlserver_remote_access_flag", + "cloudsql_instance_sqlserver_trace_flag", + "cloudsql_instance_sqlserver_user_connections_flag", + "cloudsql_instance_sqlserver_user_options_flag" ] } }, @@ -375,6 +460,16 @@ "defender_container_images_resolved_vulnerabilities", "sqlserver_microsoft_defender_enabled", "apim_threat_detection_llm_jacking" + ], + "gcp": [ + "compute_public_address_shodan", + "gcr_container_scanning_enabled", + "artifacts_container_analysis_enabled", + "compute_subnet_flow_logs_enabled", + "compute_network_dns_logging_enabled", + "cloudstorage_bucket_logging_enabled", + "compute_loadbalancer_logging_enabled", + "logging_sink_created" ] } }, @@ -408,6 +503,16 @@ "defender_attack_path_notifications_properly_configured", "vm_backup_enabled", "vm_sufficient_daily_backup_retention_period" + ], + "gcp": [ + "compute_instance_automatic_restart_enabled", + "compute_instance_group_autohealing_enabled", + "compute_instance_on_host_maintenance_migrate", + "cloudsql_instance_high_availability_enabled", + "cloudsql_instance_automated_backups", + "compute_instance_deletion_protection_enabled", + "compute_instance_group_load_balancer_attached", + "compute_instance_preemptible_vm_disabled" ] } }, @@ -459,6 +564,19 @@ "storage_blob_versioning_is_enabled", "storage_geo_redundant_enabled", "keyvault_recoverable" + ], + "gcp": [ + "cloudsql_instance_automated_backups", + "cloudsql_instance_high_availability_enabled", + "cloudstorage_bucket_versioning_enabled", + "cloudstorage_bucket_soft_delete_enabled", + "cloudstorage_bucket_lifecycle_management_enabled", + "cloudstorage_bucket_sufficient_retention_period", + "compute_instance_group_multiple_zones", + "compute_instance_disk_auto_delete_disabled", + "compute_instance_deletion_protection_enabled", + "compute_snapshot_not_outdated", + "compute_instance_suspended_without_persistent_disks" ] } }, @@ -488,6 +606,12 @@ "sqlserver_vulnerability_assessment_enabled", "sqlserver_va_periodic_recurring_scans_enabled", "sqlserver_va_scan_reports_configured" + ], + "gcp": [ + "gcr_container_scanning_enabled", + "artifacts_container_analysis_enabled", + "iam_cloud_asset_inventory_enabled", + "logging_sink_created" ] } }, @@ -517,6 +641,12 @@ "defender_ensure_notify_alerts_severity_is_high", "defender_attack_path_notifications_properly_configured", "monitor_alert_service_health_exists" + ], + "gcp": [ + "iam_organization_essential_contacts_configured", + "logging_sink_created", + "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled", + "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled" ] } }, @@ -573,6 +703,25 @@ "app_http_logs_enabled", "app_function_application_insights_enabled", "appinsights_ensure_is_configured" + ], + "gcp": [ + "iam_audit_logs_enabled", + "cloudstorage_audit_logs_enabled", + "logging_sink_created", + "cloudstorage_bucket_logging_enabled", + "cloudstorage_bucket_log_retention_policy_lock", + "cloudstorage_bucket_sufficient_retention_period", + "compute_subnet_flow_logs_enabled", + "compute_network_dns_logging_enabled", + "compute_loadbalancer_logging_enabled", + "cloudsql_instance_postgres_enable_pgaudit_flag", + "cloudsql_instance_postgres_log_connections_flag", + "cloudsql_instance_postgres_log_disconnections_flag", + "cloudsql_instance_postgres_log_statement_flag", + "cloudsql_instance_postgres_log_min_messages_flag", + "cloudsql_instance_postgres_log_error_verbosity_flag", + "cloudsql_instance_postgres_log_min_error_statement_flag", + "cloudsql_instance_postgres_log_min_duration_statement_flag" ] } }, @@ -605,6 +754,13 @@ "defender_ensure_mcas_is_enabled", "sqlserver_microsoft_defender_enabled", "apim_threat_detection_llm_jacking" + ], + "gcp": [ + "gcr_container_scanning_enabled", + "artifacts_container_analysis_enabled", + "compute_public_address_shodan", + "iam_cloud_asset_inventory_enabled", + "iam_audit_logs_enabled" ] } }, @@ -649,6 +805,19 @@ "monitor_alert_delete_public_ip_address_rule", "monitor_alert_service_health_exists", "defender_additional_email_configured_with_a_security_contact" + ], + "gcp": [ + "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled", + "logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled", + "logging_log_metric_filter_and_alert_for_compute_configuration_changes_enabled", + "logging_log_metric_filter_and_alert_for_custom_role_changes_enabled", + "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled", + "logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled", + "logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled", + "logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled", + "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled", + "iam_organization_essential_contacts_configured", + "logging_sink_created" ] } }, @@ -679,6 +848,13 @@ "sqlserver_va_periodic_recurring_scans_enabled", "vm_ensure_using_approved_images", "vm_desired_sku_size" + ], + "gcp": [ + "gcr_container_scanning_enabled", + "artifacts_container_analysis_enabled", + "compute_instance_shielded_vm_enabled", + "compute_snapshot_not_outdated", + "compute_public_address_shodan" ] } }, @@ -723,6 +899,16 @@ "app_ensure_python_version_is_latest", "app_function_latest_runtime_version", "storage_smb_protocol_version_is_latest" + ], + "gcp": [ + "gcr_container_scanning_enabled", + "artifacts_container_analysis_enabled", + "compute_snapshot_not_outdated", + "compute_network_not_legacy", + "dns_rsasha1_in_use_to_key_sign_in_dnssec", + "dns_rsasha1_in_use_to_zone_sign_in_dnssec", + "apikeys_key_rotated_in_90_days", + "iam_sa_user_managed_key_rotate_90_days" ] } }, @@ -765,6 +951,20 @@ "cosmosdb_account_use_private_endpoints", "keyvault_access_only_through_private_endpoints", "aks_clusters_created_with_private_nodes" + ], + "gcp": [ + "bigquery_dataset_public_access", + "cloudstorage_bucket_public_access", + "kms_key_not_publicly_accessible", + "cloudstorage_uses_vpc_service_controls", + "cloudfunction_function_inside_vpc", + "iam_sa_no_user_managed_keys", + "iam_sa_no_administrative_privileges", + "iam_no_service_roles_at_project_level", + "apikeys_api_restrictions_configured", + "apikeys_api_restricted_with_gemini_api", + "compute_image_not_publicly_shared", + "iam_cloud_asset_inventory_enabled" ] } }, @@ -804,6 +1004,18 @@ "app_function_identity_without_admin_privileges", "entra_policy_default_users_cannot_create_security_groups", "entra_policy_ensure_default_user_cannot_create_apps" + ], + "gcp": [ + "iam_sa_no_administrative_privileges", + "iam_no_service_roles_at_project_level", + "iam_role_kms_enforce_separation_of_duties", + "iam_role_sa_enforce_separation_of_duties", + "iam_sa_no_user_managed_keys", + "compute_instance_default_service_account_in_use", + "compute_instance_default_service_account_in_use_with_full_api_access", + "gke_cluster_no_default_service_account", + "iam_account_access_approval_enabled", + "apikeys_api_restrictions_configured" ] } }, @@ -835,6 +1047,14 @@ "defender_attack_path_notifications_properly_configured", "sqlserver_microsoft_defender_enabled", "apim_threat_detection_llm_jacking" + ], + "gcp": [ + "iam_cloud_asset_inventory_enabled", + "iam_audit_logs_enabled", + "gcr_container_scanning_enabled", + "artifacts_container_analysis_enabled", + "compute_public_address_shodan", + "logging_sink_created" ] } } From d27ec7d62e8e5db4a6c4e8cfbd614cb53dc5b0f3 Mon Sep 17 00:00:00 2001 From: s1ns3nz0 Date: Fri, 19 Jun 2026 17:21:38 +0900 Subject: [PATCH 2/5] feat(azure): add postgresql_flexible_server_geo_redundant_backup_enabled check (#11045) Co-authored-by: Daniel Barranquero --- prowler/CHANGELOG.md | 5 + prowler/compliance/azure/hipaa_azure.json | 3 +- .../compliance/azure/iso27001_2022_azure.json | 6 +- .../__init__.py | 0 ...geo_redundant_backup_enabled.metadata.json | 37 +++++ ...ble_server_geo_redundant_backup_enabled.py | 36 +++++ .../services/postgresql/postgresql_service.py | 42 +++++- ...erver_geo_redundant_backup_enabled_test.py | 132 ++++++++++++++++++ .../postgresql/postgresql_service_test.py | 79 ++++++++++- 9 files changed, 329 insertions(+), 11 deletions(-) create mode 100644 prowler/providers/azure/services/postgresql/postgresql_flexible_server_geo_redundant_backup_enabled/__init__.py create mode 100644 prowler/providers/azure/services/postgresql/postgresql_flexible_server_geo_redundant_backup_enabled/postgresql_flexible_server_geo_redundant_backup_enabled.metadata.json create mode 100644 prowler/providers/azure/services/postgresql/postgresql_flexible_server_geo_redundant_backup_enabled/postgresql_flexible_server_geo_redundant_backup_enabled.py create mode 100644 tests/providers/azure/services/postgresql/postgresql_flexible_server_geo_redundant_backup_enabled/postgresql_flexible_server_geo_redundant_backup_enabled_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 197a7f961d..c3f0fdfa9e 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -22,6 +22,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `defender_ensure_defender_cspm_is_on` check for Azure provider, verifying Microsoft Defender Cloud Security Posture Management (CSPM) is enabled on the Standard tier [(#11037)](https://github.com/prowler-cloud/prowler/pull/11037) - `mysql_flexible_server_geo_redundant_backup_enabled` check for Azure provider, verifying MySQL Flexible Servers have geo-redundant backup enabled so backups are replicated to the paired region [(#11041)](https://github.com/prowler-cloud/prowler/pull/11041) - `mysql_flexible_server_high_availability_enabled` check for Azure provider, verifying MySQL Flexible Servers have high availability enabled for automatic failover to a standby replica [(#11042)](https://github.com/prowler-cloud/prowler/pull/11042) +- `postgresql_flexible_server_geo_redundant_backup_enabled` check for Azure provider, verifying PostgreSQL Flexible Servers have geo-redundant backup enabled so backups are replicated to the paired region [(#11045)](https://github.com/prowler-cloud/prowler/pull/11045) - `aks_cluster_auto_upgrade_enabled` check for Azure provider [(#11027)](https://github.com/prowler-cloud/prowler/pull/11027) - Public `Provider.get_class()` method that resolves a provider class by name for both built-in and external (entry-point) providers [(#11398)](https://github.com/prowler-cloud/prowler/pull/11398) - Jira timeout preventing the calls from hanging indefinitely when the Jira endpoint is unreachable or slow [(#11602)](https://github.com/prowler-cloud/prowler/pull/11602) @@ -38,6 +39,10 @@ All notable changes to the **Prowler SDK** are documented in this file. - Replaced the unmaintained `awsipranges` dependency with a small standard-library helper for the `route53_dangling_ip_subdomain_takeover` check [(#9293)](https://github.com/prowler-cloud/prowler/pull/9293) +### 🐞 Fixed + +- Azure PostgreSQL flexible server inventory no longer aborts the whole subscription when the `connection_throttle.enable` parameter is missing (e.g. PostgreSQL v18), and logs the expected "Entra ID authentication not enabled" case as a warning instead of an error, so servers are still scanned [(#11045)](https://github.com/prowler-cloud/prowler/pull/11045) + ### 🔐 Security - `pytest` from 8.3.5 to 9.0.3, patching a known vulnerability in the SDK test dependency [(#11291)](https://github.com/prowler-cloud/prowler/pull/11291) diff --git a/prowler/compliance/azure/hipaa_azure.json b/prowler/compliance/azure/hipaa_azure.json index 70f1e7e13f..b27344582f 100644 --- a/prowler/compliance/azure/hipaa_azure.json +++ b/prowler/compliance/azure/hipaa_azure.json @@ -440,7 +440,8 @@ "sqlserver_auditing_retention_90_days", "postgresql_flexible_server_log_retention_days_greater_3", "cosmosdb_account_backup_policy_continuous", - "mysql_flexible_server_geo_redundant_backup_enabled" + "mysql_flexible_server_geo_redundant_backup_enabled", + "postgresql_flexible_server_geo_redundant_backup_enabled" ] }, { diff --git a/prowler/compliance/azure/iso27001_2022_azure.json b/prowler/compliance/azure/iso27001_2022_azure.json index ee0e4ca89b..fb5b2d6915 100644 --- a/prowler/compliance/azure/iso27001_2022_azure.json +++ b/prowler/compliance/azure/iso27001_2022_azure.json @@ -1268,7 +1268,8 @@ ], "Checks": [ "cosmosdb_account_backup_policy_continuous", - "mysql_flexible_server_geo_redundant_backup_enabled" + "mysql_flexible_server_geo_redundant_backup_enabled", + "postgresql_flexible_server_geo_redundant_backup_enabled" ] }, { @@ -1299,7 +1300,8 @@ "vm_trusted_launch_enabled", "cosmosdb_account_automatic_failover_enabled", "mysql_flexible_server_geo_redundant_backup_enabled", - "mysql_flexible_server_high_availability_enabled" + "mysql_flexible_server_high_availability_enabled", + "postgresql_flexible_server_geo_redundant_backup_enabled" ] }, { diff --git a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_geo_redundant_backup_enabled/__init__.py b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_geo_redundant_backup_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_geo_redundant_backup_enabled/postgresql_flexible_server_geo_redundant_backup_enabled.metadata.json b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_geo_redundant_backup_enabled/postgresql_flexible_server_geo_redundant_backup_enabled.metadata.json new file mode 100644 index 0000000000..7be125ecf2 --- /dev/null +++ b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_geo_redundant_backup_enabled/postgresql_flexible_server_geo_redundant_backup_enabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "azure", + "CheckID": "postgresql_flexible_server_geo_redundant_backup_enabled", + "CheckTitle": "PostgreSQL flexible server has geo-redundant backup enabled", + "CheckType": [], + "ServiceName": "postgresql", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "microsoft.dbforpostgresql/flexibleservers", + "ResourceGroup": "database", + "Description": "**Azure PostgreSQL Flexible Server** is evaluated for **geo-redundant backup**. Geo-redundant backup stores backup copies in a paired Azure region, enabling restore and cross-region disaster recovery.", + "Risk": "Without **geo-redundant backup**, a regional disaster can cause **permanent data loss**. Locally redundant backups only protect against storage hardware failures within the same region and cannot be restored if the primary region becomes unavailable.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/concepts-backup-restore", + "https://learn.microsoft.com/en-us/azure/templates/microsoft.dbforpostgresql/flexibleservers" + ], + "Remediation": { + "Code": { + "CLI": "az postgres flexible-server create --name --resource-group --location --geo-redundant-backup Enabled", + "NativeIaC": "```bicep\n// Bicep: PostgreSQL Flexible Server with geo-redundant backup (set at creation)\nresource postgresql 'Microsoft.DBforPostgreSQL/flexibleServers@2023-06-01-preview' = {\n name: ''\n location: ''\n properties: {\n backup: {\n geoRedundantBackup: 'Enabled' // CRITICAL: stores backups in the paired region\n }\n }\n}\n```", + "Other": "1. Geo-redundant backup must be configured when the server is created; it cannot be enabled on an existing server\n2. In the Azure portal, start creating a new Azure Database for PostgreSQL flexible server\n3. On the Basics tab, under Compute + storage, open Configure server\n4. Set Geographically redundant backup to Enabled and save\n5. Finish creating the server and migrate workloads to it", + "Terraform": "```hcl\n# Terraform: PostgreSQL Flexible Server with geo-redundant backup (set at creation)\nresource \"azurerm_postgresql_flexible_server\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n location = \"\"\n geo_redundant_backup_enabled = true # CRITICAL: stores backups in the paired region\n}\n```" + }, + "Recommendation": { + "Text": "Enable **geo-redundant backup** on PostgreSQL Flexible Servers so backups are replicated to the paired Azure region and can be restored during a regional outage. Because this is set at server creation, plan a migration for existing servers, and pair it with an appropriate backup retention period and periodic restore testing.", + "Url": "https://hub.prowler.com/check/postgresql_flexible_server_geo_redundant_backup_enabled" + } + }, + "Categories": [ + "resilience" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Geo-redundant backup for Azure PostgreSQL Flexible Server can only be configured at server creation time and cannot be changed afterwards." +} diff --git a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_geo_redundant_backup_enabled/postgresql_flexible_server_geo_redundant_backup_enabled.py b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_geo_redundant_backup_enabled/postgresql_flexible_server_geo_redundant_backup_enabled.py new file mode 100644 index 0000000000..94fdaa7a1c --- /dev/null +++ b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_geo_redundant_backup_enabled/postgresql_flexible_server_geo_redundant_backup_enabled.py @@ -0,0 +1,36 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.postgresql.postgresql_client import ( + postgresql_client, +) + + +class postgresql_flexible_server_geo_redundant_backup_enabled(Check): + """ + Ensure Azure PostgreSQL Flexible Servers have geo-redundant backup enabled. + + This check evaluates whether each Azure PostgreSQL Flexible Server stores backups in a paired Azure region, enabling cross-region disaster recovery. + + - PASS: The server has geo-redundant backup enabled (geo_redundant_backup is "Enabled"). + - FAIL: The server does not have geo-redundant backup enabled. + """ + + def execute(self) -> Check_Report_Azure: + findings = [] + for ( + subscription, + flexible_servers, + ) in postgresql_client.flexible_servers.items(): + subscription_name = postgresql_client.subscriptions.get( + subscription, subscription + ) + for server in flexible_servers: + report = Check_Report_Azure(metadata=self.metadata(), resource=server) + report.subscription = subscription + if server.geo_redundant_backup == "Enabled": + report.status = "PASS" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) has geo-redundant backup enabled." + else: + report.status = "FAIL" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) does not have geo-redundant backup enabled." + findings.append(report) + return findings diff --git a/prowler/providers/azure/services/postgresql/postgresql_service.py b/prowler/providers/azure/services/postgresql/postgresql_service.py index 13081ad270..dd8121d9dd 100644 --- a/prowler/providers/azure/services/postgresql/postgresql_service.py +++ b/prowler/providers/azure/services/postgresql/postgresql_service.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from typing import Optional from azure.mgmt.postgresqlflexibleservers import PostgreSQLManagementClient @@ -53,6 +54,7 @@ class PostgreSQL(AzureService): subscription, resource_group, postgresql_server.name ) location = server_details.location + backup = getattr(server_details, "backup", None) flexible_servers[subscription].append( Server( id=postgresql_server.id, @@ -68,6 +70,9 @@ class PostgreSQL(AzureService): connection_throttling=connection_throttling, log_retention_days=log_retention_days, firewall=firewall, + geo_redundant_backup=getattr( + backup, "geo_redundant_backup", None + ), ) ) except Exception as error: @@ -149,15 +154,37 @@ class PostgreSQL(AzureService): ) return admin_list except Exception as e: - logger.error(f"Error getting Entra ID admins for {server_name}: {e}") + if "authentication is not enabled" in str(e): + # Expected when the server uses PostgreSQL authentication only + # (Entra/Azure AD auth disabled); not an error. + logger.warning( + f"Entra ID authentication is not enabled for {server_name}; skipping Entra ID admins." + ) + else: + logger.error(f"Error getting Entra ID admins for {server_name}: {e}") return [] def _get_connection_throttling(self, subscription, resouce_group_name, server_name): client = self.clients[subscription] - connection_throttling = client.configurations.get( - resouce_group_name, server_name, "connection_throttle.enable" - ) - return connection_throttling.value.upper() + try: + connection_throttling = client.configurations.get( + resouce_group_name, server_name, "connection_throttle.enable" + ) + return connection_throttling.value.upper() + except Exception as error: + message = str(error).lower() + if "connection_throttle.enable" in message and ( + "not exist" in message or "not found" in message + ): + # The "connection_throttle.enable" parameter does not exist on + # newer PostgreSQL versions (e.g. v18); this is expected. + return None + # Any other failure is a genuine problem: surface it, but still + # degrade gracefully instead of aborting the subscription inventory. + logger.error( + f"Error getting connection throttling for {server_name}: {error}" + ) + return None def _get_log_retention_days(self, subscription, resouce_group_name, server_name): client = self.clients[subscription] @@ -214,6 +241,7 @@ class Server: log_checkpoints: str log_connections: str log_disconnections: str - connection_throttling: str - log_retention_days: str + connection_throttling: Optional[str] + log_retention_days: Optional[str] firewall: list[Firewall] + geo_redundant_backup: Optional[str] = None diff --git a/tests/providers/azure/services/postgresql/postgresql_flexible_server_geo_redundant_backup_enabled/postgresql_flexible_server_geo_redundant_backup_enabled_test.py b/tests/providers/azure/services/postgresql/postgresql_flexible_server_geo_redundant_backup_enabled/postgresql_flexible_server_geo_redundant_backup_enabled_test.py new file mode 100644 index 0000000000..db0291c989 --- /dev/null +++ b/tests/providers/azure/services/postgresql/postgresql_flexible_server_geo_redundant_backup_enabled/postgresql_flexible_server_geo_redundant_backup_enabled_test.py @@ -0,0 +1,132 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.azure.services.postgresql.postgresql_service import Server +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, + AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, + set_mocked_azure_provider, +) + + +def _make_server(server_id, server_name, geo_redundant_backup): + return Server( + id=server_id, + name=server_name, + resource_group="resource_group", + location="eastus", + require_secure_transport="ON", + active_directory_auth="Enabled", + entra_id_admins=[], + log_checkpoints="ON", + log_connections="ON", + log_disconnections="ON", + connection_throttling="ON", + log_retention_days="3", + firewall=[], + geo_redundant_backup=geo_redundant_backup, + ) + + +class Test_postgresql_flexible_server_geo_redundant_backup_enabled: + def test_no_postgresql_flexible_servers(self): + postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } + postgresql_client.flexible_servers = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.postgresql.postgresql_flexible_server_geo_redundant_backup_enabled.postgresql_flexible_server_geo_redundant_backup_enabled.postgresql_client", + new=postgresql_client, + ), + ): + from prowler.providers.azure.services.postgresql.postgresql_flexible_server_geo_redundant_backup_enabled.postgresql_flexible_server_geo_redundant_backup_enabled import ( + postgresql_flexible_server_geo_redundant_backup_enabled, + ) + + check = postgresql_flexible_server_geo_redundant_backup_enabled() + result = check.execute() + assert len(result) == 0 + + def test_postgresql_geo_redundant_backup_disabled(self): + server_id = str(uuid4()) + server_name = "test-server" + postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } + postgresql_client.flexible_servers = { + AZURE_SUBSCRIPTION_ID: [_make_server(server_id, server_name, "Disabled")] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.postgresql.postgresql_flexible_server_geo_redundant_backup_enabled.postgresql_flexible_server_geo_redundant_backup_enabled.postgresql_client", + new=postgresql_client, + ), + ): + from prowler.providers.azure.services.postgresql.postgresql_flexible_server_geo_redundant_backup_enabled.postgresql_flexible_server_geo_redundant_backup_enabled import ( + postgresql_flexible_server_geo_redundant_backup_enabled, + ) + + check = postgresql_flexible_server_geo_redundant_backup_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Flexible Postgresql server {server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have geo-redundant backup enabled." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == server_name + assert result[0].resource_id == server_id + assert result[0].location == "eastus" + + def test_postgresql_geo_redundant_backup_enabled(self): + server_id = str(uuid4()) + server_name = "test-server" + postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } + postgresql_client.flexible_servers = { + AZURE_SUBSCRIPTION_ID: [_make_server(server_id, server_name, "Enabled")] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.postgresql.postgresql_flexible_server_geo_redundant_backup_enabled.postgresql_flexible_server_geo_redundant_backup_enabled.postgresql_client", + new=postgresql_client, + ), + ): + from prowler.providers.azure.services.postgresql.postgresql_flexible_server_geo_redundant_backup_enabled.postgresql_flexible_server_geo_redundant_backup_enabled import ( + postgresql_flexible_server_geo_redundant_backup_enabled, + ) + + check = postgresql_flexible_server_geo_redundant_backup_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Flexible Postgresql server {server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has geo-redundant backup enabled." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == server_name + assert result[0].resource_id == server_id + assert result[0].location == "eastus" diff --git a/tests/providers/azure/services/postgresql/postgresql_service_test.py b/tests/providers/azure/services/postgresql/postgresql_service_test.py index f36b5675ec..c0b8bca6c4 100644 --- a/tests/providers/azure/services/postgresql/postgresql_service_test.py +++ b/tests/providers/azure/services/postgresql/postgresql_service_test.py @@ -1,4 +1,4 @@ -from unittest.mock import patch +from unittest.mock import MagicMock, patch from prowler.providers.azure.services.postgresql.postgresql_service import ( EntraIdAdmin, @@ -115,6 +115,44 @@ class Test_SqlServer_Service: == "ON" ) + def test_get_connection_throttling_missing_parameter_returns_none(self): + # PostgreSQL v18 removed the "connection_throttle.enable" parameter; the + # service must degrade gracefully (quiet None) instead of raising and + # aborting the whole subscription's server inventory. + postgresql = PostgreSQL(set_mocked_azure_provider()) + mock_client = MagicMock() + mock_client.configurations.get.side_effect = Exception( + "The configuration 'connection_throttle.enable' does not exist for " + "server version 18." + ) + postgresql.clients[AZURE_SUBSCRIPTION_ID] = mock_client + with patch( + "prowler.providers.azure.services.postgresql.postgresql_service.logger" + ) as mock_logger: + result = postgresql._get_connection_throttling( + AZURE_SUBSCRIPTION_ID, "resource_group", "server_name" + ) + assert result is None + mock_logger.error.assert_not_called() + + def test_get_connection_throttling_unexpected_error_logs_error(self): + # Any other failure (permissions, throttling, transient API errors) must + # still be logged as an error, while keeping the scan resilient (None). + postgresql = PostgreSQL(set_mocked_azure_provider()) + mock_client = MagicMock() + mock_client.configurations.get.side_effect = Exception( + "Some unexpected failure" + ) + postgresql.clients[AZURE_SUBSCRIPTION_ID] = mock_client + with patch( + "prowler.providers.azure.services.postgresql.postgresql_service.logger" + ) as mock_logger: + result = postgresql._get_connection_throttling( + AZURE_SUBSCRIPTION_ID, "resource_group", "server_name" + ) + assert result is None + mock_logger.error.assert_called_once() + def test_get_log_retention_days(self): postgesql = PostgreSQL(set_mocked_azure_provider()) assert ( @@ -138,6 +176,45 @@ class Test_SqlServer_Service: assert admins[0].principal_name == "Test Admin User" assert admins[0].object_id == "11111111-1111-1111-1111-111111111111" + def test_get_entra_id_admins_aad_not_enabled_logs_warning(self): + # A server using PostgreSQL authentication only (Entra/Azure AD auth + # disabled) is an expected state; it should be logged as a warning, not + # an error, and return an empty admin list. + postgresql = PostgreSQL(set_mocked_azure_provider()) + mock_client = MagicMock() + mock_client.administrators.list_by_server.side_effect = Exception( + "Azure AD authentication is not enabled for the given server" + ) + postgresql.clients[AZURE_SUBSCRIPTION_ID] = mock_client + with patch( + "prowler.providers.azure.services.postgresql.postgresql_service.logger" + ) as mock_logger: + result = postgresql._get_entra_id_admins( + AZURE_SUBSCRIPTION_ID, "resource_group", "server_name" + ) + assert result == [] + mock_logger.warning.assert_called_once() + mock_logger.error.assert_not_called() + + def test_get_entra_id_admins_unexpected_error_logs_error(self): + # Any other failure (permissions, throttling, transient API errors) is a + # genuine problem and must still be logged as an error. + postgresql = PostgreSQL(set_mocked_azure_provider()) + mock_client = MagicMock() + mock_client.administrators.list_by_server.side_effect = Exception( + "Some unexpected failure" + ) + postgresql.clients[AZURE_SUBSCRIPTION_ID] = mock_client + with patch( + "prowler.providers.azure.services.postgresql.postgresql_service.logger" + ) as mock_logger: + result = postgresql._get_entra_id_admins( + AZURE_SUBSCRIPTION_ID, "resource_group", "server_name" + ) + assert result == [] + mock_logger.error.assert_called_once() + mock_logger.warning.assert_not_called() + def test_get_firewall(self): postgesql = PostgreSQL(set_mocked_azure_provider()) assert ( From 9e173978dcf92f74638cf17a61d61c44f55bbcc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Fri, 19 Jun 2026 10:37:07 +0200 Subject: [PATCH 3/5] feat(compliance): add DORA compliance framework for Cloudflare (#11645) --- prowler/CHANGELOG.md | 1 + prowler/compliance/dora_2022_2554.json | 55 ++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index c3f0fdfa9e..b304ce74df 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -33,6 +33,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `entra_conditional_access_policy_no_deleted_object_references` check for M365 provider [(#11236)](https://github.com/prowler-cloud/prowler/pull/11236) - `aks_cluster_defender_enabled` check for Azure provider, verifying that AKS clusters have Microsoft Defender security monitoring enabled [(#11028)](https://github.com/prowler-cloud/prowler/pull/11028) - DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the GCP provider, mapping existing GCP checks across the five DORA pillars [(#11642)](https://github.com/prowler-cloud/prowler/pull/11642) +- DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the Cloudflare provider, mapping existing Cloudflare edge/network checks across the applicable DORA pillars [(#11645)](https://github.com/prowler-cloud/prowler/pull/11645) ### 🔄 Changed diff --git a/prowler/compliance/dora_2022_2554.json b/prowler/compliance/dora_2022_2554.json index 4a1db41beb..b6aa1e2f88 100644 --- a/prowler/compliance/dora_2022_2554.json +++ b/prowler/compliance/dora_2022_2554.json @@ -255,6 +255,18 @@ "compute_network_not_legacy", "compute_network_default_in_use", "compute_instance_single_network_interface" + ], + "cloudflare": [ + "zone_universal_ssl_enabled", + "zone_ssl_strict", + "zone_min_tls_version_secure", + "zone_tls_1_3_enabled", + "zone_hsts_enabled", + "zone_https_redirect_enabled", + "zone_automatic_https_rewrites_enabled", + "zone_development_mode_disabled", + "zone_record_caa_exists", + "zone_dnssec_enabled" ] } }, @@ -291,6 +303,11 @@ "apikeys_key_exists", "compute_instance_suspended_without_persistent_disks", "compute_public_address_shodan" + ], + "cloudflare": [ + "dns_record_no_wildcard", + "dns_record_no_internal_ip", + "dns_record_cname_target_valid" ] } }, @@ -414,6 +431,32 @@ "cloudsql_instance_sqlserver_trace_flag", "cloudsql_instance_sqlserver_user_connections_flag", "cloudsql_instance_sqlserver_user_options_flag" + ], + "cloudflare": [ + "zone_universal_ssl_enabled", + "zone_ssl_strict", + "zone_min_tls_version_secure", + "zone_tls_1_3_enabled", + "zone_hsts_enabled", + "zone_https_redirect_enabled", + "zone_automatic_https_rewrites_enabled", + "zone_record_caa_exists", + "zone_dnssec_enabled", + "zone_waf_enabled", + "zone_waf_owasp_ruleset_enabled", + "zone_firewall_blocking_rules_configured", + "zone_browser_integrity_check_enabled", + "zone_bot_fight_mode_enabled", + "zone_rate_limiting_enabled", + "zone_challenge_passage_configured", + "zone_ip_geolocation_enabled", + "dns_record_proxied", + "dns_record_no_internal_ip", + "dns_record_no_wildcard", + "dns_record_cname_target_valid", + "zone_record_spf_exists", + "zone_record_dkim_exists", + "zone_record_dmarc_exists" ] } }, @@ -470,6 +513,11 @@ "cloudstorage_bucket_logging_enabled", "compute_loadbalancer_logging_enabled", "logging_sink_created" + ], + "cloudflare": [ + "zone_bot_fight_mode_enabled", + "zone_browser_integrity_check_enabled", + "zone_rate_limiting_enabled" ] } }, @@ -909,6 +957,9 @@ "dns_rsasha1_in_use_to_zone_sign_in_dnssec", "apikeys_key_rotated_in_90_days", "iam_sa_user_managed_key_rotate_90_days" + ], + "cloudflare": [ + "zone_min_tls_version_secure" ] } }, @@ -965,6 +1016,10 @@ "apikeys_api_restricted_with_gemini_api", "compute_image_not_publicly_shared", "iam_cloud_asset_inventory_enabled" + ], + "cloudflare": [ + "zone_record_caa_exists", + "dns_record_cname_target_valid" ] } }, From bbf54011eadf3965f4ce18c28589676b644239bd Mon Sep 17 00:00:00 2001 From: s1ns3nz0 Date: Fri, 19 Jun 2026 18:59:37 +0900 Subject: [PATCH 4/5] feat(azure): add postgresql_flexible_server_high_availability_enabled check (#11046) Co-authored-by: Daniel Barranquero --- prowler/CHANGELOG.md | 1 + .../compliance/azure/iso27001_2022_azure.json | 3 +- .../__init__.py | 0 ...er_high_availability_enabled.metadata.json | 37 ++++ ...exible_server_high_availability_enabled.py | 39 ++++ .../services/postgresql/postgresql_service.py | 3 + ...e_server_high_availability_enabled_test.py | 168 ++++++++++++++++++ 7 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 prowler/providers/azure/services/postgresql/postgresql_flexible_server_high_availability_enabled/__init__.py create mode 100644 prowler/providers/azure/services/postgresql/postgresql_flexible_server_high_availability_enabled/postgresql_flexible_server_high_availability_enabled.metadata.json create mode 100644 prowler/providers/azure/services/postgresql/postgresql_flexible_server_high_availability_enabled/postgresql_flexible_server_high_availability_enabled.py create mode 100644 tests/providers/azure/services/postgresql/postgresql_flexible_server_high_availability_enabled/postgresql_flexible_server_high_availability_enabled_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index b304ce74df..2b6f400a97 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -23,6 +23,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `mysql_flexible_server_geo_redundant_backup_enabled` check for Azure provider, verifying MySQL Flexible Servers have geo-redundant backup enabled so backups are replicated to the paired region [(#11041)](https://github.com/prowler-cloud/prowler/pull/11041) - `mysql_flexible_server_high_availability_enabled` check for Azure provider, verifying MySQL Flexible Servers have high availability enabled for automatic failover to a standby replica [(#11042)](https://github.com/prowler-cloud/prowler/pull/11042) - `postgresql_flexible_server_geo_redundant_backup_enabled` check for Azure provider, verifying PostgreSQL Flexible Servers have geo-redundant backup enabled so backups are replicated to the paired region [(#11045)](https://github.com/prowler-cloud/prowler/pull/11045) +- `postgresql_flexible_server_high_availability_enabled` check for Azure provider, verifying PostgreSQL Flexible Servers have high availability enabled for automatic failover to a standby replica [(#11046)](https://github.com/prowler-cloud/prowler/pull/11046) - `aks_cluster_auto_upgrade_enabled` check for Azure provider [(#11027)](https://github.com/prowler-cloud/prowler/pull/11027) - Public `Provider.get_class()` method that resolves a provider class by name for both built-in and external (entry-point) providers [(#11398)](https://github.com/prowler-cloud/prowler/pull/11398) - Jira timeout preventing the calls from hanging indefinitely when the Jira endpoint is unreachable or slow [(#11602)](https://github.com/prowler-cloud/prowler/pull/11602) diff --git a/prowler/compliance/azure/iso27001_2022_azure.json b/prowler/compliance/azure/iso27001_2022_azure.json index fb5b2d6915..fbe2204e2d 100644 --- a/prowler/compliance/azure/iso27001_2022_azure.json +++ b/prowler/compliance/azure/iso27001_2022_azure.json @@ -1301,7 +1301,8 @@ "cosmosdb_account_automatic_failover_enabled", "mysql_flexible_server_geo_redundant_backup_enabled", "mysql_flexible_server_high_availability_enabled", - "postgresql_flexible_server_geo_redundant_backup_enabled" + "postgresql_flexible_server_geo_redundant_backup_enabled", + "postgresql_flexible_server_high_availability_enabled" ] }, { diff --git a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_high_availability_enabled/__init__.py b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_high_availability_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_high_availability_enabled/postgresql_flexible_server_high_availability_enabled.metadata.json b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_high_availability_enabled/postgresql_flexible_server_high_availability_enabled.metadata.json new file mode 100644 index 0000000000..58b6588886 --- /dev/null +++ b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_high_availability_enabled/postgresql_flexible_server_high_availability_enabled.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "azure", + "CheckID": "postgresql_flexible_server_high_availability_enabled", + "CheckTitle": "PostgreSQL flexible server has high availability enabled", + "CheckType": [], + "ServiceName": "postgresql", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "microsoft.dbforpostgresql/flexibleservers", + "ResourceGroup": "database", + "Description": "**Azure PostgreSQL Flexible Server** is evaluated for **high availability** mode. Zone-redundant or same-zone HA provisions a standby replica and provides automatic failover during planned and unplanned outages.", + "Risk": "Without **high availability**, a server or zone failure causes **downtime** until manual recovery or redeployment. Critical databases experience extended unavailability and potential data loss for in-flight transactions during unplanned outages.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/concepts-high-availability", + "https://learn.microsoft.com/en-us/azure/templates/microsoft.dbforpostgresql/flexibleservers" + ], + "Remediation": { + "Code": { + "CLI": "az postgres flexible-server update --name --resource-group --high-availability ZoneRedundant", + "NativeIaC": "```bicep\n// Bicep: PostgreSQL Flexible Server with zone-redundant high availability\nresource postgresql 'Microsoft.DBforPostgreSQL/flexibleServers@2023-06-01-preview' = {\n name: ''\n location: ''\n sku: {\n name: 'Standard_D2ds_v4'\n tier: 'GeneralPurpose'\n }\n properties: {\n highAvailability: {\n mode: 'ZoneRedundant' // CRITICAL: provisions a standby replica with automatic failover\n }\n }\n}\n```", + "Other": "1. In the Azure portal, open your Azure Database for PostgreSQL flexible server\n2. Under Settings, select High availability\n3. Enable High availability and choose Zone redundant (or Same zone)\n4. Save (high availability requires the General Purpose or Memory Optimized tier)", + "Terraform": "```hcl\n# Terraform: PostgreSQL Flexible Server with zone-redundant high availability\nresource \"azurerm_postgresql_flexible_server\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n location = \"\"\n sku_name = \"GP_Standard_D2ds_v4\"\n\n high_availability {\n mode = \"ZoneRedundant\" # CRITICAL: provisions a standby replica with automatic failover\n }\n}\n```" + }, + "Recommendation": { + "Text": "Enable **high availability** (zone-redundant where supported, otherwise same-zone) on production PostgreSQL Flexible Servers so a standby replica provides automatic failover. High availability requires the General Purpose or Memory Optimized tier; pair it with geo-redundant backup and periodic failover testing for full resilience.", + "Url": "https://hub.prowler.com/check/postgresql_flexible_server_high_availability_enabled" + } + }, + "Categories": [ + "resilience" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "High availability for Azure PostgreSQL Flexible Server requires the General Purpose or Memory Optimized compute tier and is not available on the Burstable tier." +} diff --git a/prowler/providers/azure/services/postgresql/postgresql_flexible_server_high_availability_enabled/postgresql_flexible_server_high_availability_enabled.py b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_high_availability_enabled/postgresql_flexible_server_high_availability_enabled.py new file mode 100644 index 0000000000..14c69f591e --- /dev/null +++ b/prowler/providers/azure/services/postgresql/postgresql_flexible_server_high_availability_enabled/postgresql_flexible_server_high_availability_enabled.py @@ -0,0 +1,39 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.postgresql.postgresql_client import ( + postgresql_client, +) + + +class postgresql_flexible_server_high_availability_enabled(Check): + """ + Ensure Azure PostgreSQL Flexible Servers have high availability enabled. + + This check evaluates whether each Azure PostgreSQL Flexible Server is configured with high availability (zone-redundant or same-zone), providing automatic failover to a standby replica during outages. + + - PASS: The server has high availability enabled (high_availability_mode is set and not "Disabled"). + - FAIL: The server does not have high availability enabled. + """ + + def execute(self) -> Check_Report_Azure: + findings = [] + for ( + subscription, + flexible_servers, + ) in postgresql_client.flexible_servers.items(): + subscription_name = postgresql_client.subscriptions.get( + subscription, subscription + ) + for server in flexible_servers: + report = Check_Report_Azure(metadata=self.metadata(), resource=server) + report.subscription = subscription + if ( + server.high_availability_mode is not None + and server.high_availability_mode != "Disabled" + ): + report.status = "PASS" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) has high availability enabled." + else: + report.status = "FAIL" + report.status_extended = f"Flexible Postgresql server {server.name} from subscription {subscription_name} ({subscription}) does not have high availability enabled." + findings.append(report) + return findings diff --git a/prowler/providers/azure/services/postgresql/postgresql_service.py b/prowler/providers/azure/services/postgresql/postgresql_service.py index dd8121d9dd..2048299bf1 100644 --- a/prowler/providers/azure/services/postgresql/postgresql_service.py +++ b/prowler/providers/azure/services/postgresql/postgresql_service.py @@ -55,6 +55,7 @@ class PostgreSQL(AzureService): ) location = server_details.location backup = getattr(server_details, "backup", None) + ha = getattr(server_details, "high_availability", None) flexible_servers[subscription].append( Server( id=postgresql_server.id, @@ -73,6 +74,7 @@ class PostgreSQL(AzureService): geo_redundant_backup=getattr( backup, "geo_redundant_backup", None ), + high_availability_mode=getattr(ha, "mode", None), ) ) except Exception as error: @@ -245,3 +247,4 @@ class Server: log_retention_days: Optional[str] firewall: list[Firewall] geo_redundant_backup: Optional[str] = None + high_availability_mode: Optional[str] = None diff --git a/tests/providers/azure/services/postgresql/postgresql_flexible_server_high_availability_enabled/postgresql_flexible_server_high_availability_enabled_test.py b/tests/providers/azure/services/postgresql/postgresql_flexible_server_high_availability_enabled/postgresql_flexible_server_high_availability_enabled_test.py new file mode 100644 index 0000000000..190c8629ce --- /dev/null +++ b/tests/providers/azure/services/postgresql/postgresql_flexible_server_high_availability_enabled/postgresql_flexible_server_high_availability_enabled_test.py @@ -0,0 +1,168 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.azure.services.postgresql.postgresql_service import Server +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_DISPLAY, + AZURE_SUBSCRIPTION_ID, + AZURE_SUBSCRIPTION_NAME, + set_mocked_azure_provider, +) + + +def _make_server(server_id, server_name, high_availability_mode): + return Server( + id=server_id, + name=server_name, + resource_group="resource_group", + location="eastus", + require_secure_transport="ON", + active_directory_auth="Enabled", + entra_id_admins=[], + log_checkpoints="ON", + log_connections="ON", + log_disconnections="ON", + connection_throttling="ON", + log_retention_days="3", + firewall=[], + high_availability_mode=high_availability_mode, + ) + + +class Test_postgresql_flexible_server_high_availability_enabled: + def test_no_postgresql_flexible_servers(self): + postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } + postgresql_client.flexible_servers = {} + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.postgresql.postgresql_flexible_server_high_availability_enabled.postgresql_flexible_server_high_availability_enabled.postgresql_client", + new=postgresql_client, + ), + ): + from prowler.providers.azure.services.postgresql.postgresql_flexible_server_high_availability_enabled.postgresql_flexible_server_high_availability_enabled import ( + postgresql_flexible_server_high_availability_enabled, + ) + + check = postgresql_flexible_server_high_availability_enabled() + result = check.execute() + assert len(result) == 0 + + def test_postgresql_high_availability_disabled(self): + server_id = str(uuid4()) + server_name = "test-server" + postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } + postgresql_client.flexible_servers = { + AZURE_SUBSCRIPTION_ID: [_make_server(server_id, server_name, "Disabled")] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.postgresql.postgresql_flexible_server_high_availability_enabled.postgresql_flexible_server_high_availability_enabled.postgresql_client", + new=postgresql_client, + ), + ): + from prowler.providers.azure.services.postgresql.postgresql_flexible_server_high_availability_enabled.postgresql_flexible_server_high_availability_enabled import ( + postgresql_flexible_server_high_availability_enabled, + ) + + check = postgresql_flexible_server_high_availability_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Flexible Postgresql server {server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have high availability enabled." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == server_name + assert result[0].resource_id == server_id + assert result[0].location == "eastus" + + def test_postgresql_high_availability_not_set(self): + server_id = str(uuid4()) + server_name = "test-server" + postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } + postgresql_client.flexible_servers = { + AZURE_SUBSCRIPTION_ID: [_make_server(server_id, server_name, None)] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.postgresql.postgresql_flexible_server_high_availability_enabled.postgresql_flexible_server_high_availability_enabled.postgresql_client", + new=postgresql_client, + ), + ): + from prowler.providers.azure.services.postgresql.postgresql_flexible_server_high_availability_enabled.postgresql_flexible_server_high_availability_enabled import ( + postgresql_flexible_server_high_availability_enabled, + ) + + check = postgresql_flexible_server_high_availability_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Flexible Postgresql server {server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} does not have high availability enabled." + ) + + def test_postgresql_high_availability_enabled(self): + server_id = str(uuid4()) + server_name = "test-server" + postgresql_client = mock.MagicMock + postgresql_client.subscriptions = { + AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME + } + postgresql_client.flexible_servers = { + AZURE_SUBSCRIPTION_ID: [ + _make_server(server_id, server_name, "ZoneRedundant") + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.postgresql.postgresql_flexible_server_high_availability_enabled.postgresql_flexible_server_high_availability_enabled.postgresql_client", + new=postgresql_client, + ), + ): + from prowler.providers.azure.services.postgresql.postgresql_flexible_server_high_availability_enabled.postgresql_flexible_server_high_availability_enabled import ( + postgresql_flexible_server_high_availability_enabled, + ) + + check = postgresql_flexible_server_high_availability_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Flexible Postgresql server {server_name} from subscription {AZURE_SUBSCRIPTION_DISPLAY} has high availability enabled." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == server_name + assert result[0].resource_id == server_id + assert result[0].location == "eastus" From e10cf34ad66b77dd1913ac56ec9bb5f3ba36c373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Mart=C3=ADn?= Date: Fri, 19 Jun 2026 12:17:33 +0200 Subject: [PATCH 5/5] feat(compliance): DORA compliance framework for Alibaba Cloud (#11646) --- prowler/CHANGELOG.md | 1 + prowler/compliance/dora_2022_2554.json | 134 +++++++++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 2b6f400a97..8801277aab 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -35,6 +35,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `aks_cluster_defender_enabled` check for Azure provider, verifying that AKS clusters have Microsoft Defender security monitoring enabled [(#11028)](https://github.com/prowler-cloud/prowler/pull/11028) - DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the GCP provider, mapping existing GCP checks across the five DORA pillars [(#11642)](https://github.com/prowler-cloud/prowler/pull/11642) - DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the Cloudflare provider, mapping existing Cloudflare edge/network checks across the applicable DORA pillars [(#11645)](https://github.com/prowler-cloud/prowler/pull/11645) +- DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the AlibabaCloud provider, mapping existing AlibabaCloud checks across the applicable DORA pillars [(#11646)](https://github.com/prowler-cloud/prowler/pull/11646) ### 🔄 Changed diff --git a/prowler/compliance/dora_2022_2554.json b/prowler/compliance/dora_2022_2554.json index b6aa1e2f88..e4b5fd69df 100644 --- a/prowler/compliance/dora_2022_2554.json +++ b/prowler/compliance/dora_2022_2554.json @@ -144,6 +144,22 @@ "compute_instance_default_service_account_in_use", "compute_instance_default_service_account_in_use_with_full_api_access", "gke_cluster_no_default_service_account" + ], + "alibabacloud": [ + "ram_no_root_access_key", + "ram_user_mfa_enabled_console_access", + "ram_user_console_access_unused", + "ram_rotate_access_key_90_days", + "ram_password_policy_minimum_length", + "ram_password_policy_lowercase", + "ram_password_policy_uppercase", + "ram_password_policy_number", + "ram_password_policy_symbol", + "ram_password_policy_password_reuse_prevention", + "ram_password_policy_max_password_age", + "ram_password_policy_max_login_attempts", + "ram_policy_attached_only_to_group_or_roles", + "ram_policy_no_administrative_privileges" ] } }, @@ -192,6 +208,12 @@ "gcr_container_scanning_enabled", "artifacts_container_analysis_enabled", "iam_organization_essential_contacts_configured" + ], + "alibabacloud": [ + "securitycenter_advanced_or_enterprise_edition", + "securitycenter_all_assets_agent_installed", + "securitycenter_vulnerability_scan_enabled", + "actiontrail_multi_region_enabled" ] } }, @@ -267,6 +289,15 @@ "zone_development_mode_disabled", "zone_record_caa_exists", "zone_dnssec_enabled" + ], + "alibabacloud": [ + "oss_bucket_secure_transport_enabled", + "rds_instance_ssl_enabled", + "rds_instance_tde_enabled", + "rds_instance_tde_key_custom", + "ecs_attached_disk_encrypted", + "ecs_unattached_disk_encrypted", + "ecs_instance_no_legacy_network" ] } }, @@ -308,6 +339,10 @@ "dns_record_no_wildcard", "dns_record_no_internal_ip", "dns_record_cname_target_valid" + ], + "alibabacloud": [ + "securitycenter_all_assets_agent_installed", + "ram_user_console_access_unused" ] } }, @@ -457,6 +492,25 @@ "zone_record_spf_exists", "zone_record_dkim_exists", "zone_record_dmarc_exists" + ], + "alibabacloud": [ + "oss_bucket_not_publicly_accessible", + "oss_bucket_secure_transport_enabled", + "actiontrail_oss_bucket_not_publicly_accessible", + "ecs_attached_disk_encrypted", + "ecs_unattached_disk_encrypted", + "ecs_securitygroup_restrict_ssh_internet", + "ecs_securitygroup_restrict_rdp_internet", + "ecs_instance_endpoint_protection_installed", + "rds_instance_no_public_access_whitelist", + "rds_instance_ssl_enabled", + "rds_instance_tde_enabled", + "rds_instance_tde_key_custom", + "cs_kubernetes_network_policy_enabled", + "cs_kubernetes_eni_multiple_ip_enabled", + "cs_kubernetes_private_cluster_enabled", + "cs_kubernetes_rbac_enabled", + "cs_kubernetes_dashboard_disabled" ] } }, @@ -518,6 +572,13 @@ "zone_bot_fight_mode_enabled", "zone_browser_integrity_check_enabled", "zone_rate_limiting_enabled" + ], + "alibabacloud": [ + "securitycenter_vulnerability_scan_enabled", + "securitycenter_notification_enabled_high_risk", + "securitycenter_all_assets_agent_installed", + "ecs_instance_endpoint_protection_installed", + "cs_kubernetes_cloudmonitor_enabled" ] } }, @@ -561,6 +622,11 @@ "compute_instance_deletion_protection_enabled", "compute_instance_group_load_balancer_attached", "compute_instance_preemptible_vm_disabled" + ], + "alibabacloud": [ + "securitycenter_notification_enabled_high_risk", + "cs_kubernetes_cluster_check_recent", + "cs_kubernetes_cluster_check_weekly" ] } }, @@ -660,6 +726,11 @@ "artifacts_container_analysis_enabled", "iam_cloud_asset_inventory_enabled", "logging_sink_created" + ], + "alibabacloud": [ + "securitycenter_vulnerability_scan_enabled", + "securitycenter_all_assets_agent_installed", + "ecs_instance_latest_os_patches_applied" ] } }, @@ -695,6 +766,9 @@ "logging_sink_created", "logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled", "logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled" + ], + "alibabacloud": [ + "securitycenter_notification_enabled_high_risk" ] } }, @@ -770,6 +844,19 @@ "cloudsql_instance_postgres_log_error_verbosity_flag", "cloudsql_instance_postgres_log_min_error_statement_flag", "cloudsql_instance_postgres_log_min_duration_statement_flag" + ], + "alibabacloud": [ + "actiontrail_multi_region_enabled", + "actiontrail_oss_bucket_not_publicly_accessible", + "oss_bucket_logging_enabled", + "sls_logstore_retention_period", + "vpc_flow_logs_enabled", + "cs_kubernetes_log_service_enabled", + "rds_instance_sql_audit_enabled", + "rds_instance_sql_audit_retention", + "rds_instance_postgresql_log_connections_enabled", + "rds_instance_postgresql_log_disconnections_enabled", + "rds_instance_postgresql_log_duration_enabled" ] } }, @@ -809,6 +896,10 @@ "compute_public_address_shodan", "iam_cloud_asset_inventory_enabled", "iam_audit_logs_enabled" + ], + "alibabacloud": [ + "securitycenter_notification_enabled_high_risk", + "securitycenter_vulnerability_scan_enabled" ] } }, @@ -866,6 +957,21 @@ "logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled", "iam_organization_essential_contacts_configured", "logging_sink_created" + ], + "alibabacloud": [ + "sls_management_console_authentication_failures_alert_enabled", + "sls_management_console_signin_without_mfa_alert_enabled", + "sls_root_account_usage_alert_enabled", + "sls_unauthorized_api_calls_alert_enabled", + "sls_ram_role_changes_alert_enabled", + "sls_oss_bucket_policy_changes_alert_enabled", + "sls_oss_permission_changes_alert_enabled", + "sls_rds_instance_configuration_changes_alert_enabled", + "sls_security_group_changes_alert_enabled", + "sls_vpc_changes_alert_enabled", + "sls_vpc_network_route_changes_alert_enabled", + "sls_cloud_firewall_changes_alert_enabled", + "sls_customer_created_cmk_changes_alert_enabled" ] } }, @@ -903,6 +1009,13 @@ "compute_instance_shielded_vm_enabled", "compute_snapshot_not_outdated", "compute_public_address_shodan" + ], + "alibabacloud": [ + "securitycenter_vulnerability_scan_enabled", + "securitycenter_advanced_or_enterprise_edition", + "ecs_instance_latest_os_patches_applied", + "cs_kubernetes_cluster_check_recent", + "cs_kubernetes_cluster_check_weekly" ] } }, @@ -960,6 +1073,11 @@ ], "cloudflare": [ "zone_min_tls_version_secure" + ], + "alibabacloud": [ + "securitycenter_vulnerability_scan_enabled", + "ecs_instance_latest_os_patches_applied", + "ecs_instance_no_legacy_network" ] } }, @@ -1020,6 +1138,11 @@ "cloudflare": [ "zone_record_caa_exists", "dns_record_cname_target_valid" + ], + "alibabacloud": [ + "ram_policy_no_administrative_privileges", + "oss_bucket_not_publicly_accessible", + "actiontrail_oss_bucket_not_publicly_accessible" ] } }, @@ -1071,6 +1194,11 @@ "gke_cluster_no_default_service_account", "iam_account_access_approval_enabled", "apikeys_api_restrictions_configured" + ], + "alibabacloud": [ + "ram_policy_no_administrative_privileges", + "ram_policy_attached_only_to_group_or_roles", + "ram_no_root_access_key" ] } }, @@ -1110,6 +1238,12 @@ "artifacts_container_analysis_enabled", "compute_public_address_shodan", "logging_sink_created" + ], + "alibabacloud": [ + "securitycenter_advanced_or_enterprise_edition", + "securitycenter_notification_enabled_high_risk", + "actiontrail_multi_region_enabled", + "sls_logstore_retention_period" ] } }