diff --git a/prowler/providers/aws/services/rds/rds_instance_deprecated_engine_version/__init__.py b/prowler/providers/aws/services/rds/rds_instance_deprecated_engine_version/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/prowler/providers/aws/services/rds/rds_instance_deprecated_engine_version/rds_instance_deprecated_engine_version.metadata.json b/prowler/providers/aws/services/rds/rds_instance_deprecated_engine_version/rds_instance_deprecated_engine_version.metadata.json new file mode 100644 index 00000000..1b035509 --- /dev/null +++ b/prowler/providers/aws/services/rds/rds_instance_deprecated_engine_version/rds_instance_deprecated_engine_version.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "aws", + "CheckID": "rds_instance_deprecated_engine_version", + "CheckTitle": "Check if RDS instance is using a supported engine version", + "CheckType": [], + "ServiceName": "rds", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", + "Severity": "medium", + "ResourceType": "AwsRdsDbInstance", + "Description": "Check if RDS is using a supported engine version for MariaDB, MySQL and PostgreSQL", + "Risk": "If not enabled RDS instances may be vulnerable to security issues", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "aws rds describe-db-engine-versions --engine '", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/cli/latest/reference/rds/describe-db-engine-versions.html", + "Terraform": "" + }, + "Recommendation": { + "Text": "", + "Url": "" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/rds/rds_instance_deprecated_engine_version/rds_instance_deprecated_engine_version.py b/prowler/providers/aws/services/rds/rds_instance_deprecated_engine_version/rds_instance_deprecated_engine_version.py new file mode 100644 index 00000000..a9376b90 --- /dev/null +++ b/prowler/providers/aws/services/rds/rds_instance_deprecated_engine_version/rds_instance_deprecated_engine_version.py @@ -0,0 +1,28 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.rds.rds_client import rds_client + + +class rds_instance_deprecated_engine_version(Check): + def execute(self): + findings = [] + + for instance in rds_client.db_instances: + report = Check_Report_AWS(self.metadata()) + report.region = instance.region + report.status = "FAIL" + report.resource_id = instance.id + report.resource_tags = instance.tags + report.status_extended = f"RDS instance {instance.id} is using a deprecated engine {instance.engine} with version {instance.engine_version}." + + if ( + instance.engine_version + in rds_client.db_engines[instance.region][ + instance.engine + ].engine_versions + ): + report.status = "PASS" + report.status_extended = f"RDS instance {instance.id} is not using a deprecated engine {instance.engine} with version {instance.engine_version}." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/rds/rds_instance_transport_encrypted/rds_instance_transport_encrypted.py b/prowler/providers/aws/services/rds/rds_instance_transport_encrypted/rds_instance_transport_encrypted.py index a18b5005..fff4c280 100644 --- a/prowler/providers/aws/services/rds/rds_instance_transport_encrypted/rds_instance_transport_encrypted.py +++ b/prowler/providers/aws/services/rds/rds_instance_transport_encrypted/rds_instance_transport_encrypted.py @@ -10,6 +10,7 @@ class rds_instance_transport_encrypted(Check): report = Check_Report_AWS(self.metadata()) report.region = db_instance.region report.resource_id = db_instance.id + report.resource_tags = db_instance.tags report.status = "FAIL" report.status_extended = ( f"RDS Instance {db_instance.id} connections are not encrypted." diff --git a/prowler/providers/aws/services/rds/rds_service.py b/prowler/providers/aws/services/rds/rds_service.py index cc191f57..b893cb28 100644 --- a/prowler/providers/aws/services/rds/rds_service.py +++ b/prowler/providers/aws/services/rds/rds_service.py @@ -20,6 +20,7 @@ class RDS: self.db_instances = [] self.db_clusters = {} self.db_snapshots = [] + self.db_engines = {} self.db_cluster_snapshots = [] self.__threading_call__(self.__describe_db_instances__) self.__threading_call__(self.__describe_db_parameters__) @@ -28,6 +29,7 @@ class RDS: self.__threading_call__(self.__describe_db_clusters__) self.__threading_call__(self.__describe_db_cluster_snapshots__) self.__threading_call__(self.__describe_db_cluster_snapshot_attributes__) + self.__threading_call__(self.__describe_db_engine_versions__) def __get_session__(self): return self.session @@ -60,6 +62,7 @@ class RDS: id=instance["DBInstanceIdentifier"], endpoint=instance.get("Endpoint"), engine=instance["Engine"], + engine_version=instance["EngineVersion"], status=instance["DBInstanceStatus"], public=instance["PubliclyAccessible"], encrypted=instance["StorageEncrypted"], @@ -249,11 +252,42 @@ class RDS: f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + def __describe_db_engine_versions__(self, regional_client): + logger.info("RDS - Describe Engine Versions...") + try: + describe_db_engine_versions_paginator = regional_client.get_paginator( + "describe_db_engine_versions" + ) + for page in describe_db_engine_versions_paginator.paginate(): + for engine in page["DBEngineVersions"]: + if regional_client.region not in self.db_engines: + self.db_engines[regional_client.region] = {} + if engine["Engine"] not in self.db_engines[regional_client.region]: + db_engine = DBEngine( + region=regional_client.region, + engine=engine["Engine"], + engine_versions=[engine["EngineVersion"]], + engine_description=engine["DBEngineDescription"], + ) + self.db_engines[regional_client.region][ + engine["Engine"] + ] = db_engine + else: + self.db_engines[regional_client.region][ + engine["Engine"] + ].engine_versions.append(engine["EngineVersion"]) + + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + class DBInstance(BaseModel): id: str endpoint: Optional[dict] engine: str + engine_version: str status: str public: bool encrypted: bool @@ -301,3 +335,10 @@ class ClusterSnapshot(BaseModel): public: bool = False region: str tags: Optional[list] = [] + + +class DBEngine(BaseModel): + region: str + engine: str + engine_versions: list[str] + engine_description: str diff --git a/tests/providers/aws/services/rds/rds_instance_deprecated_engine_version/rds_instance_deprecated_engine_version_test.py b/tests/providers/aws/services/rds/rds_instance_deprecated_engine_version/rds_instance_deprecated_engine_version_test.py new file mode 100644 index 00000000..d4eb0dfc --- /dev/null +++ b/tests/providers/aws/services/rds/rds_instance_deprecated_engine_version/rds_instance_deprecated_engine_version_test.py @@ -0,0 +1,161 @@ +from unittest import mock +from unittest.mock import patch + +import botocore +from boto3 import client, session +from moto import mock_rds + +from prowler.providers.aws.lib.audit_info.models import AWS_Audit_Info + +AWS_ACCOUNT_NUMBER = "123456789012" +AWS_REGION = "us-east-1" + +make_api_call = botocore.client.BaseClient._make_api_call + + +def mock_make_api_call(self, operation_name, kwarg): + if operation_name == "DescribeDBEngineVersions": + return { + "DBEngineVersions": [ + { + "Engine": "mysql", + "EngineVersion": "8.0.32", + "DBEngineDescription": "description", + "DBEngineVersionDescription": "description", + }, + ] + } + return make_api_call(self, operation_name, kwarg) + + +@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) +class Test_rds_instance_deprecated_engine_version: + # Mocked Audit Info + def set_mocked_audit_info(self): + audit_info = AWS_Audit_Info( + session_config=None, + original_session=None, + audit_session=session.Session( + profile_name=None, + botocore_session=None, + region_name=AWS_REGION, + ), + audited_account=AWS_ACCOUNT_NUMBER, + audited_user_id=None, + audited_partition="aws", + audited_identity_arn=None, + profile=None, + profile_region=AWS_REGION, + credentials=None, + assumed_role_info=None, + audited_regions=[AWS_REGION], + organizations_metadata=None, + audit_resources=None, + ) + return audit_info + + @mock_rds + def test_rds_no_instances(self): + from prowler.providers.aws.services.rds.rds_service import RDS + + audit_info = self.set_mocked_audit_info() + + with mock.patch( + "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", + new=audit_info, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_deprecated_engine_version.rds_instance_deprecated_engine_version.rds_client", + new=RDS(audit_info), + ): + # Test Check + from prowler.providers.aws.services.rds.rds_instance_deprecated_engine_version.rds_instance_deprecated_engine_version import ( + rds_instance_deprecated_engine_version, + ) + + check = rds_instance_deprecated_engine_version() + result = check.execute() + + assert len(result) == 0 + + @mock_rds + def test_rds_instance_no_deprecated_engine_version(self): + conn = client("rds", region_name=AWS_REGION) + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + AllocatedStorage=10, + Engine="mysql", + EngineVersion="8.0.32", + DBName="staging-mysql", + DBInstanceClass="db.m1.small", + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + audit_info = self.set_mocked_audit_info() + + with mock.patch( + "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", + new=audit_info, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_deprecated_engine_version.rds_instance_deprecated_engine_version.rds_client", + new=RDS(audit_info), + ): + # Test Check + from prowler.providers.aws.services.rds.rds_instance_deprecated_engine_version.rds_instance_deprecated_engine_version import ( + rds_instance_deprecated_engine_version, + ) + + check = rds_instance_deprecated_engine_version() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "RDS instance db-master-1 is not using a deprecated engine mysql with version 8.0.32." + ) + assert result[0].resource_id == "db-master-1" + assert result[0].resource_tags == [] + + @mock_rds + def test_rds_instance_deprecated_engine_version(self): + conn = client("rds", region_name=AWS_REGION) + conn.create_db_instance( + DBInstanceIdentifier="db-master-2", + AllocatedStorage=10, + Engine="mysql", + EngineVersion="8.0.23", + DBName="staging-mysql", + DBInstanceClass="db.m1.small", + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + audit_info = self.set_mocked_audit_info() + + with mock.patch( + "prowler.providers.aws.lib.audit_info.audit_info.current_audit_info", + new=audit_info, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_deprecated_engine_version.rds_instance_deprecated_engine_version.rds_client", + new=RDS(audit_info), + ): + # Test Check + from prowler.providers.aws.services.rds.rds_instance_deprecated_engine_version.rds_instance_deprecated_engine_version import ( + rds_instance_deprecated_engine_version, + ) + + check = rds_instance_deprecated_engine_version() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "RDS instance db-master-2 is using a deprecated engine mysql with version 8.0.23." + ) + assert result[0].resource_id == "db-master-2" + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/rds/rds_instance_multi_az/rds_instance_multi_az_test.py b/tests/providers/aws/services/rds/rds_instance_multi_az/rds_instance_multi_az_test.py index 0f9859aa..73b907b6 100644 --- a/tests/providers/aws/services/rds/rds_instance_multi_az/rds_instance_multi_az_test.py +++ b/tests/providers/aws/services/rds/rds_instance_multi_az/rds_instance_multi_az_test.py @@ -163,6 +163,7 @@ class Test_rds_instance_multi_az: id="test-instance", endpoint="", engine="aurora", + engine_version="1.0.0", status="available", public=False, encrypted=False, @@ -223,6 +224,7 @@ class Test_rds_instance_multi_az: id="test-instance", endpoint="", engine="aurora", + engine_version="1.0.0", status="available", public=False, encrypted=False, diff --git a/tests/providers/aws/services/rds/rds_service_test.py b/tests/providers/aws/services/rds/rds_service_test.py index 22e9524f..6cb95f7f 100644 --- a/tests/providers/aws/services/rds/rds_service_test.py +++ b/tests/providers/aws/services/rds/rds_service_test.py @@ -1,3 +1,6 @@ +from unittest.mock import patch + +import botocore from boto3 import client, session from moto import mock_rds @@ -7,7 +10,25 @@ from prowler.providers.aws.services.rds.rds_service import RDS AWS_ACCOUNT_NUMBER = "123456789012" AWS_REGION = "us-east-1" +make_api_call = botocore.client.BaseClient._make_api_call + +def mock_make_api_call(self, operation_name, kwarg): + if operation_name == "DescribeDBEngineVersions": + return { + "DBEngineVersions": [ + { + "Engine": "mysql", + "EngineVersion": "8.0.32", + "DBEngineDescription": "description", + "DBEngineVersionDescription": "description", + }, + ] + } + return make_api_call(self, operation_name, kwarg) + + +@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) class Test_RDS_Service: # Mocked Audit Info def set_mocked_audit_info(self): @@ -26,7 +47,7 @@ class Test_RDS_Service: profile_region=None, credentials=None, assumed_role_info=None, - audited_regions=None, + audited_regions=[AWS_REGION], organizations_metadata=None, audit_resources=None, ) @@ -249,3 +270,13 @@ class Test_RDS_Service: assert rds.db_cluster_snapshots[0].cluster_id == "db-primary-1" assert rds.db_cluster_snapshots[0].region == AWS_REGION assert not rds.db_cluster_snapshots[0].public + + # Test RDS describe db engine versions + @mock_rds + def test__describe_db_engine_versions__(self): + # RDS client for this test class + audit_info = self.set_mocked_audit_info() + rds = RDS(audit_info) + assert "mysql" in rds.db_engines[AWS_REGION] + assert rds.db_engines[AWS_REGION]["mysql"].engine_versions == ["8.0.32"] + assert rds.db_engines[AWS_REGION]["mysql"].engine_description == "description"