From eee35f9cc36843a12c1441e7fbc2e31ef1796740 Mon Sep 17 00:00:00 2001 From: Gabriel Soltz Date: Wed, 19 Apr 2023 12:26:20 +0200 Subject: [PATCH] feat(ssmincidents): New Service and Checks (#2219) Co-authored-by: Sergio Garcia Co-authored-by: Pepe Fagoaga --- .../aws/services/ssmincidents/__init__.py | 0 .../ssmincidents/ssmincidents_client.py | 6 + .../__init__.py | 0 ...incidents_enabled_with_plans.metadata.json | 32 ++++ .../ssmincidents_enabled_with_plans.py | 27 +++ .../ssmincidents/ssmincidents_service.py | 163 ++++++++++++++++++ .../ssmincidents_enabled_with_plans_test.py | 125 ++++++++++++++ .../ssmincidents/ssmincidents_service_test.py | 135 +++++++++++++++ 8 files changed, 488 insertions(+) create mode 100644 prowler/providers/aws/services/ssmincidents/__init__.py create mode 100644 prowler/providers/aws/services/ssmincidents/ssmincidents_client.py create mode 100644 prowler/providers/aws/services/ssmincidents/ssmincidents_enabled_with_plans/__init__.py create mode 100644 prowler/providers/aws/services/ssmincidents/ssmincidents_enabled_with_plans/ssmincidents_enabled_with_plans.metadata.json create mode 100644 prowler/providers/aws/services/ssmincidents/ssmincidents_enabled_with_plans/ssmincidents_enabled_with_plans.py create mode 100644 prowler/providers/aws/services/ssmincidents/ssmincidents_service.py create mode 100644 tests/providers/aws/services/ssmincidents/ssmincidents_enabled_with_plans/ssmincidents_enabled_with_plans_test.py create mode 100644 tests/providers/aws/services/ssmincidents/ssmincidents_service_test.py diff --git a/prowler/providers/aws/services/ssmincidents/__init__.py b/prowler/providers/aws/services/ssmincidents/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/prowler/providers/aws/services/ssmincidents/ssmincidents_client.py b/prowler/providers/aws/services/ssmincidents/ssmincidents_client.py new file mode 100644 index 00000000..d71616fd --- /dev/null +++ b/prowler/providers/aws/services/ssmincidents/ssmincidents_client.py @@ -0,0 +1,6 @@ +from prowler.providers.aws.lib.audit_info.audit_info import current_audit_info +from prowler.providers.aws.services.ssmincidents.ssmincidents_service import ( + SSMIncidents, +) + +ssmincidents_client = SSMIncidents(current_audit_info) diff --git a/prowler/providers/aws/services/ssmincidents/ssmincidents_enabled_with_plans/__init__.py b/prowler/providers/aws/services/ssmincidents/ssmincidents_enabled_with_plans/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/prowler/providers/aws/services/ssmincidents/ssmincidents_enabled_with_plans/ssmincidents_enabled_with_plans.metadata.json b/prowler/providers/aws/services/ssmincidents/ssmincidents_enabled_with_plans/ssmincidents_enabled_with_plans.metadata.json new file mode 100644 index 00000000..1cfccf4c --- /dev/null +++ b/prowler/providers/aws/services/ssmincidents/ssmincidents_enabled_with_plans/ssmincidents_enabled_with_plans.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "ssmincidents_enabled_with_plans", + "CheckTitle": "Ensure SSM Incidents is enabled with response plans.", + "CheckType": [], + "ServiceName": "ssm", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:ssm:region:account-id:document/document-name", + "Severity": "medium", + "ResourceType": "Other", + "Description": "Ensure SSM Incidents is enabled with response plans.", + "Risk": "Not having SSM Incidents enabled can increase the risk of delayed detection and response to security incidents, unauthorized access, limited visibility into incidents and vulnerabilities", + "RelatedUrl": "https://docs.aws.amazon.com/incident-manager/latest/userguide/response-plans.html", + "Remediation": { + "Code": { + "CLI": "aws ssm-incidents create-response-plan", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable SSM Incidents and create response plans", + "Url": "" + } + }, + "Categories": [ + "" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/ssmincidents/ssmincidents_enabled_with_plans/ssmincidents_enabled_with_plans.py b/prowler/providers/aws/services/ssmincidents/ssmincidents_enabled_with_plans/ssmincidents_enabled_with_plans.py new file mode 100644 index 00000000..68a1d400 --- /dev/null +++ b/prowler/providers/aws/services/ssmincidents/ssmincidents_enabled_with_plans/ssmincidents_enabled_with_plans.py @@ -0,0 +1,27 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.ssmincidents.ssmincidents_client import ( + ssmincidents_client, +) + + +class ssmincidents_enabled_with_plans(Check): + def execute(self): + findings = [] + report = Check_Report_AWS(self.metadata()) + report.status = "FAIL" + report.status_extended = "No SSM Incidents replication set exists." + report.resource_id = "SSMIncidents" + report.region = ssmincidents_client.region + if ssmincidents_client.replication_set: + report.resource_arn = ssmincidents_client.replication_set[0].arn + report.resource_tags = [] # Not supported for replication sets + report.status_extended = f"SSM Incidents replication set {ssmincidents_client.replication_set[0].arn} exists but not ACTIVE." + if ssmincidents_client.replication_set[0].status == "ACTIVE": + report.status_extended = f"SSM Incidents replication set {ssmincidents_client.replication_set[0].arn} is ACTIVE but no response plans exist." + if ssmincidents_client.response_plans: + report.status = "PASS" + report.status_extended = f"SSM Incidents replication set {ssmincidents_client.replication_set[0].arn} is ACTIVE and has response plans." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/ssmincidents/ssmincidents_service.py b/prowler/providers/aws/services/ssmincidents/ssmincidents_service.py new file mode 100644 index 00000000..5deaefcb --- /dev/null +++ b/prowler/providers/aws/services/ssmincidents/ssmincidents_service.py @@ -0,0 +1,163 @@ +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 + +# Note: +# This service is a bit special because it creates a resource (Replication Set) in one region, but you can list it in from any region using list_replication_sets +# The ARN of this resource, doesn't include the region: arn:aws:ssm-incidents:::replication-set/, so is listed the same way in any region. +# The problem is that for doing a get_replication_set, we need the region where the replication set was created or any regions where it is replicating. +# Because we need to do a get_replication_set to describe it and we don't know the region, we iterate across all regions until we find it, once we find it, we stop iterating. + + +################## SSMIncidents +class SSMIncidents: + def __init__(self, audit_info): + self.service = "ssm-incidents" + 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.replication_set = [] + self.__list_replication_sets__() + self.__get_replication_set__() + self.response_plans = [] + self.__threading_call__(self.__list_response_plans__) + self.__list_tags_for_resource__() + + 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 __list_replication_sets__(self): + logger.info("SSMIncidents - Listing Replication Sets...") + try: + regional_client = self.regional_clients[self.region] + list_replication_sets = regional_client.list_replication_sets()[ + "replicationSetArns" + ] + if list_replication_sets: + replication_set = list_replication_sets[0] + if not self.audit_resources or ( + is_resource_filtered(replication_set, self.audit_resources) + ): + self.replication_set = [ + ReplicationSet( + arn=replication_set, + ) + ] + except Exception as error: + logger.error( + f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}" + ) + + def __get_replication_set__(self): + logger.info("SSMIncidents - Getting Replication Sets...") + try: + replication_set = self.replication_set[0] + for regional_client in self.regional_clients.values(): + try: + get_replication_set = regional_client.get_replication_set( + arn=replication_set.arn + )["replicationSet"] + replication_set.status = get_replication_set["status"] + for region in get_replication_set["regionMap"]: + replication_set.region_map.append( + RegionMap( + status=get_replication_set["regionMap"][region][ + "status" + ], + region=region, + sse_kms_id=get_replication_set["regionMap"][region][ + "sseKmsKeyId" + ], + ) + ) + break # We found the replication set, we stop iterating + except ClientError as error: + if error.response["Error"]["Code"] == "ResourceNotFoundException": + # The replication set is not in this region, we continue to the next region + continue + 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}" + ) + + def __list_response_plans__(self, regional_client): + logger.info("SSMIncidents - Listing Response Plans...") + try: + list_response_plans_paginator = regional_client.get_paginator( + "list_response_plans" + ) + for page in list_response_plans_paginator.paginate(): + for response_plan in page["responsePlanSummaries"]: + self.response_plans.append( + ResponsePlan( + arn=response_plan["Arn"], + region=regional_client.region, + name=response_plan["Name"], + ) + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}" + ) + + def __list_tags_for_resource__(self): + logger.info("SSMIncidents - List Tags...") + try: + for response_plan in self.response_plans: + regional_client = self.regional_clients[response_plan.region] + response = regional_client.list_tags_for_resource( + resourceArn=response_plan.arn + )["tags"] + response_plan.tags = response + + except Exception as error: + logger.error( + f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}" + ) + + +class RegionMap(BaseModel): + status: str + region: str + sse_kms_id: str + + +class ReplicationSet(BaseModel): + arn: str + status: str = None + region_map: list[RegionMap] = [] + + +class ResponsePlan(BaseModel): + arn: str + name: str + region: str + tags: list = None diff --git a/tests/providers/aws/services/ssmincidents/ssmincidents_enabled_with_plans/ssmincidents_enabled_with_plans_test.py b/tests/providers/aws/services/ssmincidents/ssmincidents_enabled_with_plans/ssmincidents_enabled_with_plans_test.py new file mode 100644 index 00000000..cac74d90 --- /dev/null +++ b/tests/providers/aws/services/ssmincidents/ssmincidents_enabled_with_plans/ssmincidents_enabled_with_plans_test.py @@ -0,0 +1,125 @@ +from unittest import mock + +from prowler.providers.aws.services.ssmincidents.ssmincidents_service import ( + ReplicationSet, + ResponsePlan, +) + +AWS_REGION = "us-east-1" +REPLICATION_SET_ARN = "arn:aws:ssm-incidents::111122223333:replication-set/40bd98f0-4110-2dee-b35e-b87006f9e172" +RESPONSE_PLAN_ARN = "arn:aws:ssm-incidents::111122223333:response-plan/example-response" + + +class Test_ssmincidents_enabled_with_plans: + def test_ssmincidents_no_replicationset(self): + ssmincidents_client = mock.MagicMock + ssmincidents_client.region = AWS_REGION + ssmincidents_client.replication_set = [] + with mock.patch( + "prowler.providers.aws.services.ssmincidents.ssmincidents_service.SSMIncidents", + new=ssmincidents_client, + ): + # Test Check + from prowler.providers.aws.services.ssmincidents.ssmincidents_enabled_with_plans.ssmincidents_enabled_with_plans import ( + ssmincidents_enabled_with_plans, + ) + + check = ssmincidents_enabled_with_plans() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended == "No SSM Incidents replication set exists." + ) + assert result[0].resource_id == "SSMIncidents" + assert result[0].resource_arn == "" + assert result[0].region == AWS_REGION + + def test_ssmincidents_replicationset_not_active(self): + ssmincidents_client = mock.MagicMock + ssmincidents_client.region = AWS_REGION + ssmincidents_client.replication_set = [ + ReplicationSet(arn=REPLICATION_SET_ARN, status="CREATING") + ] + with mock.patch( + "prowler.providers.aws.services.ssmincidents.ssmincidents_service.SSMIncidents", + new=ssmincidents_client, + ): + # Test Check + from prowler.providers.aws.services.ssmincidents.ssmincidents_enabled_with_plans.ssmincidents_enabled_with_plans import ( + ssmincidents_enabled_with_plans, + ) + + check = ssmincidents_enabled_with_plans() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"SSM Incidents replication set {REPLICATION_SET_ARN} exists but not ACTIVE." + ) + assert result[0].resource_id == "SSMIncidents" + assert result[0].resource_arn == REPLICATION_SET_ARN + assert result[0].region == AWS_REGION + + def test_ssmincidents_replicationset_active_no_plans(self): + ssmincidents_client = mock.MagicMock + ssmincidents_client.region = AWS_REGION + ssmincidents_client.replication_set = [ + ReplicationSet(arn=REPLICATION_SET_ARN, status="ACTIVE") + ] + ssmincidents_client.response_plans = [] + with mock.patch( + "prowler.providers.aws.services.ssmincidents.ssmincidents_service.SSMIncidents", + new=ssmincidents_client, + ): + # Test Check + from prowler.providers.aws.services.ssmincidents.ssmincidents_enabled_with_plans.ssmincidents_enabled_with_plans import ( + ssmincidents_enabled_with_plans, + ) + + check = ssmincidents_enabled_with_plans() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"SSM Incidents replication set {REPLICATION_SET_ARN} is ACTIVE but no response plans exist." + ) + assert result[0].resource_id == "SSMIncidents" + assert result[0].resource_arn == REPLICATION_SET_ARN + assert result[0].region == AWS_REGION + + def test_ssmincidents_replicationset_active_with_plans(self): + ssmincidents_client = mock.MagicMock + ssmincidents_client.region = AWS_REGION + ssmincidents_client.replication_set = [ + ReplicationSet(arn=REPLICATION_SET_ARN, status="ACTIVE") + ] + ssmincidents_client.response_plans = [ + ResponsePlan(arn=RESPONSE_PLAN_ARN, name="test", region=AWS_REGION) + ] + with mock.patch( + "prowler.providers.aws.services.ssmincidents.ssmincidents_service.SSMIncidents", + new=ssmincidents_client, + ): + # Test Check + from prowler.providers.aws.services.ssmincidents.ssmincidents_enabled_with_plans.ssmincidents_enabled_with_plans import ( + ssmincidents_enabled_with_plans, + ) + + check = ssmincidents_enabled_with_plans() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"SSM Incidents replication set {REPLICATION_SET_ARN} is ACTIVE and has response plans." + ) + assert result[0].resource_id == "SSMIncidents" + assert result[0].resource_arn == REPLICATION_SET_ARN + assert result[0].region == AWS_REGION diff --git a/tests/providers/aws/services/ssmincidents/ssmincidents_service_test.py b/tests/providers/aws/services/ssmincidents/ssmincidents_service_test.py new file mode 100644 index 00000000..a4bdf83b --- /dev/null +++ b/tests/providers/aws/services/ssmincidents/ssmincidents_service_test.py @@ -0,0 +1,135 @@ +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.ssmincidents.ssmincidents_service import ( + SSMIncidents, +) + +# Mock Test Region +AWS_REGION = "us-east-1" +REPLICATION_SET_ARN = "arn:aws:ssm-incidents::111122223333:replication-set/40bd98f0-4110-2dee-b35e-b87006f9e172" +RESPONSE_PLAN_ARN = "arn:aws:ssm-incidents::111122223333:response-plan/example-response" + +# 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 == "ListReplicationSets": + return {"replicationSetArns": [REPLICATION_SET_ARN]} + if operation_name == "GetReplicationSet": + return { + "replicationSet": { + "arn": REPLICATION_SET_ARN, + "createdBy": "Prowler", + "createdTime": datetime(2024, 1, 1), + "deletionProtected": False, + "lastModifiedBy": datetime(2024, 1, 1), + "lastModifiedTime": datetime(2024, 1, 1), + "regionMap": { + AWS_REGION: { + "sseKmsKeyId": "DefaultKey", + "status": "ACTIVE", + "statusMessage": "Test", + "statusUpdateDateTime": datetime(2024, 1, 1), + } + }, + "status": "ACTIVE", + } + } + if operation_name == "ListResponsePlans": + return { + "responsePlanSummaries": [ + {"Arn": RESPONSE_PLAN_ARN, "displayName": "test", "Name": "test"} + ] + } + if operation_name == "ListTagsForResource": + return {"tags": {"tag_test": "tag_value"}} + + 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.ssmincidents.ssmincidents_service.generate_regional_clients", + new=mock_generate_regional_clients, +) +class Test_SSMIncidents_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() + ssmincidents = SSMIncidents(audit_info) + assert ( + ssmincidents.regional_clients[AWS_REGION].__class__.__name__ + == "SSMIncidents" + ) + + def test__get_service__(self): + audit_info = self.set_mocked_audit_info() + ssmincidents = SSMIncidents(audit_info) + assert ssmincidents.service == "ssm-incidents" + + def test__list_replication_sets__(self): + audit_info = self.set_mocked_audit_info() + ssmincidents = SSMIncidents(audit_info) + assert len(ssmincidents.replication_set) == 1 + + def test__get_replication_set__(self): + audit_info = self.set_mocked_audit_info() + ssmincidents = SSMIncidents(audit_info) + assert ssmincidents.replication_set[0].arn == REPLICATION_SET_ARN + assert ssmincidents.replication_set[0].status == "ACTIVE" + for region in ssmincidents.replication_set[0].region_map: + assert region.region == AWS_REGION + assert region.status == "ACTIVE" + assert region.sse_kms_id == "DefaultKey" + + def test__list_response_plans__(self): + audit_info = self.set_mocked_audit_info() + ssmincidents = SSMIncidents(audit_info) + assert len(ssmincidents.response_plans) == 1 + assert ssmincidents.response_plans[0].arn == RESPONSE_PLAN_ARN + assert ssmincidents.response_plans[0].name == "test" + assert ssmincidents.response_plans[0].region == AWS_REGION + assert ssmincidents.response_plans[0].tags == {"tag_test": "tag_value"} + + def test__list_tags_for_resource__(self): + audit_info = self.set_mocked_audit_info() + ssmincidents = SSMIncidents(audit_info) + assert len(ssmincidents.response_plans) == 1 + assert ssmincidents.response_plans[0].tags == {"tag_test": "tag_value"}