diff --git a/prowler/providers/aws/services/drs/__init__.py b/prowler/providers/aws/services/drs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/prowler/providers/aws/services/drs/drs_client.py b/prowler/providers/aws/services/drs/drs_client.py new file mode 100644 index 00000000..0ab6efb3 --- /dev/null +++ b/prowler/providers/aws/services/drs/drs_client.py @@ -0,0 +1,4 @@ +from prowler.providers.aws.lib.audit_info.audit_info import current_audit_info +from prowler.providers.aws.services.drs.drs_service import DRS + +drs_client = DRS(current_audit_info) diff --git a/prowler/providers/aws/services/drs/drs_job_exist/__init__.py b/prowler/providers/aws/services/drs/drs_job_exist/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/prowler/providers/aws/services/drs/drs_job_exist/drs_job_exist.metadata.json b/prowler/providers/aws/services/drs/drs_job_exist/drs_job_exist.metadata.json new file mode 100644 index 00000000..2e3c8e32 --- /dev/null +++ b/prowler/providers/aws/services/drs/drs_job_exist/drs_job_exist.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "drs_job_exist", + "CheckTitle": "Ensure DRS is enabled with jobs.", + "CheckType": [], + "ServiceName": "drs", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:drs:region:account-id:job/job-id", + "Severity": "medium", + "ResourceType": "Other", + "Description": "Ensure DRS is enabled with jobs.", + "Risk": "If DRS is not enabled with jobs, then it may not be able to recover from a disaster.", + "RelatedUrl": "https://docs.aws.amazon.com/drs/latest/userguide/what-is-drs.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Ensure DRS is enabled with jobs.", + "Url": "https://docs.aws.amazon.com/drs/latest/userguide/what-is-drs.html" + } + }, + "Categories": [ + "" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/drs/drs_job_exist/drs_job_exist.py b/prowler/providers/aws/services/drs/drs_job_exist/drs_job_exist.py new file mode 100644 index 00000000..924bbc58 --- /dev/null +++ b/prowler/providers/aws/services/drs/drs_job_exist/drs_job_exist.py @@ -0,0 +1,24 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.drs.drs_client import drs_client + + +class drs_job_exist(Check): + def execute(self): + findings = [] + for drs in drs_client.drs_services: + report = Check_Report_AWS(self.metadata()) + report.status = "FAIL" + report.status_extended = "DRS is not enabled for this region." + report.resource_id = drs.id + report.region = drs.region + report.resource_tags = [] + report.resource_arn = "" + if drs.status == "ENABLED": + report.status_extended = "DRS is enabled for this region without jobs." + if drs.jobs: + report.status = "PASS" + report.status_extended = "DRS is enabled for this region with jobs." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/drs/drs_service.py b/prowler/providers/aws/services/drs/drs_service.py new file mode 100644 index 00000000..ea24736b --- /dev/null +++ b/prowler/providers/aws/services/drs/drs_service.py @@ -0,0 +1,99 @@ +import threading + +from botocore.client import ClientError +from pydantic import BaseModel + +from prowler.lib.logger import logger +from prowler.lib.scan_filters.scan_filters import is_resource_filtered +from prowler.providers.aws.aws_provider import generate_regional_clients + +################## DRS (Elastic Disaster Recovery Service) + + +class DRS: + def __init__(self, audit_info): + self.service = "drs" + self.session = audit_info.audit_session + self.audited_account = audit_info.audited_account + self.audited_partition = audit_info.audited_partition + self.audit_resources = audit_info.audit_resources + self.regional_clients = generate_regional_clients(self.service, audit_info) + # If the region is not set in the audit profile, + # we pick the first region from the regional clients list + self.region = ( + audit_info.profile_region + if audit_info.profile_region + else list(self.regional_clients.keys())[0] + ) + self.drs_services = [] + self.__threading_call__(self.__describe_jobs__) + + def __get_session__(self): + return self.session + + def __threading_call__(self, call): + threads = [] + for regional_client in self.regional_clients.values(): + threads.append(threading.Thread(target=call, args=(regional_client,))) + for t in threads: + t.start() + for t in threads: + t.join() + + def __describe_jobs__(self, regional_client): + logger.info("DRS - Describe Jobs...") + try: + try: + describe_jobs_paginator = regional_client.get_paginator("describe_jobs") + for page in describe_jobs_paginator.paginate(): + drs_jobs = [] + for drs_job in page["items"]: + if not self.audit_resources or ( + is_resource_filtered(drs_job["arn"], self.audit_resources) + ): + job = Job( + arn=drs_job.get("arn"), + id=drs_job.get("jobID"), + region=regional_client.region, + status=drs_job.get("status"), + tags=[drs_job.get("tags")], + ) + drs_jobs.append(job) + self.drs_services.append( + DRSservice( + id="DRS", + status="ENABLED", + region=regional_client.region, + jobs=drs_jobs, + ) + ) + except ClientError as error: + if error.response["Error"]["Code"] == "UninitializedAccountException": + self.drs_services.append( + DRSservice( + id="DRS", status="DISABLED", region=regional_client.region + ) + ) + else: + logger.error( + f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}" + ) + + +class Job(BaseModel): + arn: str + id: str + status: str + region: str + tags: list = [] + + +class DRSservice(BaseModel): + id: str + status: str + region: str + jobs: list[Job] = [] diff --git a/tests/providers/aws/services/drs/drs_job_exist/drs_job_exist_test.py b/tests/providers/aws/services/drs/drs_job_exist/drs_job_exist_test.py new file mode 100644 index 00000000..4cda2c8b --- /dev/null +++ b/tests/providers/aws/services/drs/drs_job_exist/drs_job_exist_test.py @@ -0,0 +1,114 @@ +from unittest import mock + +from prowler.providers.aws.services.drs.drs_service import DRSservice, Job + +AWS_REGION = "eu-west-1" +JOB_ARN = "arn:aws:drs:eu-west-1:123456789012:job/12345678901234567890123456789012" + + +class Test_drs_job_exist: + def test_drs_job_exist(self): + drs_client = mock.MagicMock + drs_client.region = AWS_REGION + drs_client.drs_services = [ + DRSservice( + id="DRS", + status="ENABLED", + region=AWS_REGION, + jobs=[ + Job( + arn=JOB_ARN, + id="12345678901234567890123456789012", + status="COMPLETED", + region=AWS_REGION, + tags=[{"Key": "Name", "Value": "test"}], + ) + ], + ) + ] + with mock.patch( + "prowler.providers.aws.services.drs.drs_service.DRS", + new=drs_client, + ): + # Test Check + from prowler.providers.aws.services.drs.drs_job_exist.drs_job_exist import ( + drs_job_exist, + ) + + check = drs_job_exist() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended == "DRS is enabled for this region with jobs." + ) + assert result[0].resource_id == "DRS" + assert result[0].resource_arn == "" + assert result[0].region == AWS_REGION + assert result[0].resource_tags == [] + + def test_drs_no_jobs(self): + drs_client = mock.MagicMock + drs_client.region = AWS_REGION + drs_client.drs_services = [ + DRSservice( + id="DRS", + status="ENABLED", + region=AWS_REGION, + jobs=[], + ) + ] + with mock.patch( + "prowler.providers.aws.services.drs.drs_service.DRS", + new=drs_client, + ): + # Test Check + from prowler.providers.aws.services.drs.drs_job_exist.drs_job_exist import ( + drs_job_exist, + ) + + check = drs_job_exist() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "DRS is enabled for this region without jobs." + ) + assert result[0].resource_id == "DRS" + assert result[0].resource_arn == "" + assert result[0].region == AWS_REGION + assert result[0].resource_tags == [] + + def test_drs_disabled(self): + drs_client = mock.MagicMock + drs_client.region = AWS_REGION + drs_client.drs_services = [ + DRSservice( + id="DRS", + status="DISABLED", + region=AWS_REGION, + jobs=[], + ) + ] + with mock.patch( + "prowler.providers.aws.services.drs.drs_service.DRS", + new=drs_client, + ): + # Test Check + from prowler.providers.aws.services.drs.drs_job_exist.drs_job_exist import ( + drs_job_exist, + ) + + check = drs_job_exist() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended == "DRS is not enabled for this region." + assert result[0].resource_id == "DRS" + assert result[0].resource_arn == "" + assert result[0].region == AWS_REGION + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/drs/drs_service_test.py b/tests/providers/aws/services/drs/drs_service_test.py new file mode 100644 index 00000000..5211afca --- /dev/null +++ b/tests/providers/aws/services/drs/drs_service_test.py @@ -0,0 +1,98 @@ +from datetime import datetime +from unittest.mock import patch + +import botocore +from boto3 import session + +from prowler.providers.aws.lib.audit_info.audit_info import AWS_Audit_Info +from prowler.providers.aws.services.drs.drs_service import DRS + +# Mock Test Region +AWS_REGION = "us-east-1" + +# Mocking Calls +make_api_call = botocore.client.BaseClient._make_api_call + + +def mock_make_api_call(self, operation_name, kwargs): + """We have to mock every AWS API call using Boto3""" + if operation_name == "DescribeJobs": + return { + "items": [ + { + "arn": "arn:aws:disaster-recovery:us-east-1:123456789012:job/jobID1", + "creationDateTime": datetime(2024, 1, 1), + "endDateTime": datetime(2024, 1, 1), + "initiatedBy": "START_RECOVERY", + "jobID": "jobID1", + "participatingServers": [ + { + "launchStatus": "PENDING", + "recoveryInstanceID": "i-1234567890abcdef0", + "sourceServerID": "i-1234567890abcdef0", + }, + ], + "status": "PENDING", + "tags": {"test_tag": "test_value"}, + "type": "LAUNCH", + }, + ] + } + + return make_api_call(self, operation_name, kwargs) + + +def mock_generate_regional_clients(service, audit_info): + regional_client = audit_info.audit_session.client(service, region_name=AWS_REGION) + regional_client.region = AWS_REGION + return {AWS_REGION: regional_client} + + +# Patch every AWS call using Boto3 and generate_regional_clients to have 1 client +@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) +@patch( + "prowler.providers.aws.services.drs.drs_service.generate_regional_clients", + new=mock_generate_regional_clients, +) +class Test_DRS_Service: + + # 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, + ), + audited_account=None, + audited_user_id=None, + audited_partition="aws", + audited_identity_arn=None, + profile=None, + profile_region=None, + credentials=None, + assumed_role_info=None, + audited_regions=None, + organizations_metadata=None, + audit_resources=None, + ) + return audit_info + + def test__get_client__(self): + audit_info = self.set_mocked_audit_info() + drs = DRS(audit_info) + assert drs.regional_clients[AWS_REGION].__class__.__name__ == "drs" + + def test__get_service__(self): + audit_info = self.set_mocked_audit_info() + drs = DRS(audit_info) + assert drs.service == "drs" + + def test__describe_jobs__(self): + audit_info = self.set_mocked_audit_info() + drs = DRS(audit_info) + assert len(drs.drs_services) == 1 + assert drs.drs_services[0].id == "DRS" + assert drs.drs_services[0].region == AWS_REGION + assert drs.drs_services[0].status == "ENABLED"