From 04e2d15dd2ce7ed6a3cc10551692ab770c5e2618 Mon Sep 17 00:00:00 2001 From: Kay Agahd Date: Wed, 28 Jan 2026 16:49:35 +0100 Subject: [PATCH] feat(aws): add check rds_instance_extended_support (#9865) Co-authored-by: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> --- prowler/CHANGELOG.md | 1 + .../rds_instance_extended_support/__init__.py | 0 ...ds_instance_extended_support.metadata.json | 41 +++++ .../rds_instance_extended_support.py | 37 +++++ .../providers/aws/services/rds/rds_service.py | 4 + .../rds_instance_extended_support_test.py | 155 ++++++++++++++++++ 6 files changed, 238 insertions(+) create mode 100644 prowler/providers/aws/services/rds/rds_instance_extended_support/__init__.py create mode 100644 prowler/providers/aws/services/rds/rds_instance_extended_support/rds_instance_extended_support.metadata.json create mode 100644 prowler/providers/aws/services/rds/rds_instance_extended_support/rds_instance_extended_support.py create mode 100644 tests/providers/aws/services/rds/rds_instance_extended_support/rds_instance_extended_support_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 933cc87190..130675c1e3 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `exchange_shared_mailbox_sign_in_disabled` check for M365 provider [(#9828)](https://github.com/prowler-cloud/prowler/pull/9828) - CloudTrail Timeline abstraction for querying resource modification history [(#9101)](https://github.com/prowler-cloud/prowler/pull/9101) - Cloudflare `--account-id` filter argument [(#9894)](https://github.com/prowler-cloud/prowler/pull/9894) +- `rds_instance_extended_support` check for AWS provider [(#9865)](https://github.com/prowler-cloud/prowler/pull/9865) ### Changed diff --git a/prowler/providers/aws/services/rds/rds_instance_extended_support/__init__.py b/prowler/providers/aws/services/rds/rds_instance_extended_support/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/rds/rds_instance_extended_support/rds_instance_extended_support.metadata.json b/prowler/providers/aws/services/rds/rds_instance_extended_support/rds_instance_extended_support.metadata.json new file mode 100644 index 0000000000..c22a81a675 --- /dev/null +++ b/prowler/providers/aws/services/rds/rds_instance_extended_support/rds_instance_extended_support.metadata.json @@ -0,0 +1,41 @@ +{ + "Provider": "aws", + "CheckID": "rds_instance_extended_support", + "CheckTitle": "RDS instance is not enrolled in RDS Extended Support", + "CheckType": [ + "Software and Configuration Checks/Patch Management", + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "rds", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "AwsRdsDbInstance", + "ResourceGroup": "database", + "Description": "**RDS DB instances** are evaluated for enrollment in Amazon RDS Extended Support. The check fails if `EngineLifecycleSupportis` set to `open-source-rds-extended-support`, indicating the instance will incur additional charges after standard support ends.", + "Risk": "DB instances enrolled in RDS Extended Support can incur additional charges after the end of standard support for the running database major version. Remaining on older major versions can also delay necessary upgrades, increasing operational and security risk.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/extended-support-viewing.html", + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/extended-support-charges.html", + "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/extended-support-creating-db-instance.html" + ], + "Remediation": { + "Code": { + "CLI": "aws rds modify-db-instance --db-instance-identifier --engine-version --allow-major-version-upgrade --apply-immediately\n# For new DB instances created via automation, prevent enrollment by setting the lifecycle option:\naws rds create-db-instance ... --engine-lifecycle-support open-source-rds-extended-support-disabled", + "NativeIaC": "```yaml\n# CloudFormation: upgrade RDS engine version for an existing instance\nResources:\n :\n Type: AWS::RDS::DBInstance\n Properties:\n DBInstanceIdentifier: \n Engine: \n DBInstanceClass: db.t3.micro\n EngineVersion: # CRITICAL: move to a supported engine version\n AllowMajorVersionUpgrade: true # CRITICAL: required if upgrading major version\n ApplyImmediately: true # CRITICAL: apply change now to pass the check\n```", + "Other": "If your automation (CloudFormation/Terraform/SDK) creates or restores DB instances, set EngineLifecycleSupport/LifeCycleSupport to open-source-rds-extended-support-disabled where supported, and ensure your upgrade process keeps engines within standard support.", + "Terraform": "```hcl\n# Upgrade RDS engine version\nresource \"aws_db_instance\" \"\" {\n identifier = \"\"\n engine = \"\"\n instance_class = \"db.t3.micro\"\n allocated_storage = 20\n\n engine_version = \"\" # CRITICAL: use a supported version\n allow_major_version_upgrade = true # CRITICAL: needed for major upgrades\n apply_immediately = true # CRITICAL: apply now to pass the check\n}\n```" + }, + "Recommendation": { + "Text": "Upgrade enrolled DB instances to an engine version covered under standard support to stop Extended Support charges. For new DB instances and restores created via automation, explicitly set the engine lifecycle support option to avoid unintended enrollment in RDS Extended Support when that is your policy.", + "Url": "https://hub.prowler.com/check/rds_instance_extended_support" + } + }, + "Categories": [ + "vulnerabilities" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/rds/rds_instance_extended_support/rds_instance_extended_support.py b/prowler/providers/aws/services/rds/rds_instance_extended_support/rds_instance_extended_support.py new file mode 100644 index 0000000000..6caee8b808 --- /dev/null +++ b/prowler/providers/aws/services/rds/rds_instance_extended_support/rds_instance_extended_support.py @@ -0,0 +1,37 @@ +""" +Prowler check: rds_instance_extended_support + +This check fails when an RDS DB instance is enrolled in Amazon RDS Extended Support. +Enrollment is exposed via the "EngineLifecycleSupport" attribute returned by DescribeDBInstances. +""" + +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.rds.rds_client import rds_client + + +class rds_instance_extended_support(Check): + def execute(self): + findings = [] + + for db_instance in rds_client.db_instances.values(): + report = Check_Report_AWS(metadata=self.metadata(), resource=db_instance) + + # EngineLifecycleSupport can be absent when Extended Support is not applicable. + lifecycle_support = getattr(db_instance, "engine_lifecycle_support", None) + + if lifecycle_support == "open-source-rds-extended-support": + report.status = "FAIL" + report.status_extended = ( + f"RDS instance {db_instance.id} ({db_instance.engine} {db_instance.engine_version}) " + f"is enrolled in RDS Extended Support (EngineLifecycleSupport={lifecycle_support})." + ) + else: + report.status = "PASS" + report.status_extended = ( + f"RDS instance {db_instance.id} ({db_instance.engine} {db_instance.engine_version}) " + "is not enrolled in RDS Extended Support." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/rds/rds_service.py b/prowler/providers/aws/services/rds/rds_service.py index 4a1022daaa..7828978653 100644 --- a/prowler/providers/aws/services/rds/rds_service.py +++ b/prowler/providers/aws/services/rds/rds_service.py @@ -59,6 +59,9 @@ class RDS(AWSService): endpoint=instance.get("Endpoint", {}), engine=instance["Engine"], engine_version=instance["EngineVersion"], + engine_lifecycle_support=instance.get( + "EngineLifecycleSupport" + ), status=instance["DBInstanceStatus"], public=instance.get("PubliclyAccessible", False), encrypted=instance["StorageEncrypted"], @@ -531,6 +534,7 @@ class DBInstance(BaseModel): endpoint: dict engine: str engine_version: str + engine_lifecycle_support: Optional[str] = None status: str public: bool encrypted: bool diff --git a/tests/providers/aws/services/rds/rds_instance_extended_support/rds_instance_extended_support_test.py b/tests/providers/aws/services/rds/rds_instance_extended_support/rds_instance_extended_support_test.py new file mode 100644 index 0000000000..fd1c79cbb5 --- /dev/null +++ b/tests/providers/aws/services/rds/rds_instance_extended_support/rds_instance_extended_support_test.py @@ -0,0 +1,155 @@ +from unittest import mock +from unittest.mock import patch + +import botocore +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + +make_api_call = botocore.client.BaseClient._make_api_call + + +def mock_make_api_call(self, operation_name, kwarg): + """ + Moto's RDS implementation does not currently expose EngineLifecycleSupport on DescribeDBInstances. + This patch injects it into the response so that Prowler's RDS service can map it onto the DBInstance model. + + The check under test fails when: + EngineLifecycleSupport == "open-source-rds-extended-support" + """ + response = make_api_call(self, operation_name, kwarg) + + if operation_name == "DescribeDBInstances": + for instance in response.get("DBInstances", []): + if instance.get("DBInstanceIdentifier") == "db-extended-1": + instance["EngineLifecycleSupport"] = "open-source-rds-extended-support" + return response + + return response + + +@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) +class Test_rds_instance_extended_support: + @mock_aws + def test_rds_no_instances(self): + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_extended_support.rds_instance_extended_support.rds_client", + new=RDS(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.rds.rds_instance_extended_support.rds_instance_extended_support import ( + rds_instance_extended_support, + ) + + check = rds_instance_extended_support() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_rds_instance_not_enrolled_in_extended_support(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_instance( + DBInstanceIdentifier="db-standard-1", + AllocatedStorage=10, + Engine="postgres", + EngineVersion="8.0.32", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + PubliclyAccessible=False, + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_extended_support.rds_instance_extended_support.rds_client", + new=RDS(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.rds.rds_instance_extended_support.rds_instance_extended_support import ( + rds_instance_extended_support, + ) + + check = rds_instance_extended_support() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "RDS instance db-standard-1 (postgres 8.0.32) is not enrolled in RDS Extended Support." + ) + assert result[0].resource_id == "db-standard-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-standard-1" + ) + assert result[0].resource_tags == [] + + @mock_aws + def test_rds_instance_enrolled_in_extended_support(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_instance( + DBInstanceIdentifier="db-extended-1", + AllocatedStorage=10, + Engine="postgres", + EngineVersion="8.0.32", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + PubliclyAccessible=False, + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_extended_support.rds_instance_extended_support.rds_client", + new=RDS(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.rds.rds_instance_extended_support.rds_instance_extended_support import ( + rds_instance_extended_support, + ) + + check = rds_instance_extended_support() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "RDS instance db-extended-1 (postgres 8.0.32) is enrolled in RDS Extended Support " + "(EngineLifecycleSupport=open-source-rds-extended-support)." + ) + assert result[0].resource_id == "db-extended-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-extended-1" + ) + assert result[0].resource_tags == []