diff --git a/prowler/__main__.py b/prowler/__main__.py index f9c321d579..db6ec266aa 100644 --- a/prowler/__main__.py +++ b/prowler/__main__.py @@ -52,12 +52,7 @@ from prowler.lib.outputs.slack.slack import Slack from prowler.lib.outputs.summary_table import display_summary_table from prowler.providers.aws.lib.s3.s3 import send_to_s3_bucket -from prowler.providers.aws.lib.security_hub.security_hub import ( - batch_send_to_security_hub, - filter_security_hub_findings_per_region, - resolve_security_hub_previous_findings, - verify_security_hub_integration_enabled_per_region, -) +from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub from prowler.providers.common.provider import Provider from prowler.providers.common.quick_inventory import run_provider_quick_inventory @@ -360,35 +355,24 @@ def prowler(): print( f"{Style.BRIGHT}\nSending findings to AWS Security Hub, please wait...{Style.RESET_ALL}" ) - # Verify where AWS Security Hub is enabled - aws_security_enabled_regions = [] + security_hub_regions = ( global_provider.get_available_aws_service_regions("securityhub") if not global_provider.identity.audited_regions else global_provider.identity.audited_regions ) - for region in security_hub_regions: - # Save the regions where AWS Security Hub is enabled - if verify_security_hub_integration_enabled_per_region( - global_provider.identity.partition, - region, - global_provider.session.current_session, - global_provider.identity.account, - ): - aws_security_enabled_regions.append(region) - - # Prepare the findings to be sent to Security Hub - security_hub_findings_per_region = filter_security_hub_findings_per_region( - asff_output.data, - global_provider.output_options.send_sh_only_fails, - global_provider.output_options.status, - aws_security_enabled_regions, + + security_hub = SecurityHub( + aws_account_id=global_provider.identity.account, + aws_partition=global_provider.identity.partition, + aws_session=global_provider.session.current_session, + findings=asff_output.data, + status=global_provider.output_options.status, + send_only_fails=global_provider.output_options.send_sh_only_fails, + aws_security_hub_available_regions=security_hub_regions, ) # Send the findings to Security Hub - findings_sent_to_security_hub = batch_send_to_security_hub( - security_hub_findings_per_region, global_provider.session.current_session - ) - + findings_sent_to_security_hub = security_hub.batch_send_to_security_hub() print( f"{Style.BRIGHT}{Fore.GREEN}\n{findings_sent_to_security_hub} findings sent to AWS Security Hub!{Style.RESET_ALL}" ) @@ -398,10 +382,7 @@ def prowler(): print( f"{Style.BRIGHT}\nArchiving previous findings in AWS Security Hub, please wait...{Style.RESET_ALL}" ) - findings_archived_in_security_hub = resolve_security_hub_previous_findings( - security_hub_findings_per_region, - global_provider, - ) + findings_archived_in_security_hub = security_hub.archive_previous_findings() print( f"{Style.BRIGHT}{Fore.GREEN}\n{findings_archived_in_security_hub} findings archived in AWS Security Hub!{Style.RESET_ALL}" ) diff --git a/prowler/lib/outputs/asff/asff.py b/prowler/lib/outputs/asff/asff.py index 5ae31fde45..18a0d37a10 100644 --- a/prowler/lib/outputs/asff/asff.py +++ b/prowler/lib/outputs/asff/asff.py @@ -82,7 +82,11 @@ def transform(self, findings: list[Finding]) -> None: ), GeneratorId="prowler-" + finding.check_id, AwsAccountId=finding.account_uid, - Types=finding.check_type.split(","), + Types=( + finding.check_type.split(",") + if finding.check_type + else ["Software and Configuration Checks"] + ), FirstObservedAt=timestamp, UpdatedAt=timestamp, CreatedAt=timestamp, diff --git a/prowler/providers/aws/lib/security_hub/security_hub.py b/prowler/providers/aws/lib/security_hub/security_hub.py index 6fc0517ee4..646dce0e1a 100644 --- a/prowler/providers/aws/lib/security_hub/security_hub.py +++ b/prowler/providers/aws/lib/security_hub/security_hub.py @@ -1,4 +1,4 @@ -from boto3 import session +from boto3 import Session from botocore.client import ClientError from prowler.config.config import timestamp_utc @@ -9,254 +9,288 @@ SECURITY_HUB_MAX_BATCH = 100 -def filter_security_hub_findings_per_region( - findings: list[AWSSecurityFindingFormat], - send_only_fails: bool, - status: list, - enabled_regions: list, -) -> dict: - """filter_security_hub_findings_per_region filters the findings by region and status. It returns a dictionary with the findings per region. - - Args: - findings (list[AWSSecurityFindingFormat]): List of findings - send_only_fails (bool): Send only the findings that have failed - status (list): List of statuses to filter the findings - enabled_regions (list): List of enabled regions - - Returns: - dict: Dictionary containing the findings per region +class SecurityHub: """ - security_hub_findings_per_region = {} - # Create a key per audited region - for region in enabled_regions: - security_hub_findings_per_region[region] = [] - for finding in findings: - # We don't send findings to not enabled regions - if finding.Resources[0].Region not in enabled_regions: - continue - - if ( - finding.Compliance.Status != "FAILED" - or finding.Compliance.Status == "WARNING" - ) and send_only_fails: - continue - - # SecurityHub valid statuses are: PASSED, FAILED, WARNING - if status: - if finding.Compliance.Status == "PASSED" and "PASS" not in status: - continue - if finding.Compliance.Status == "FAILED" and "FAIL" not in status: - continue - # Check muted finding - if finding.Compliance.Status == "WARNING": - continue - - # Get the finding region - # We can do that since the finding always stores just one finding - region = finding.Resources[0].Region - - # Include that finding within their region - security_hub_findings_per_region[region].append(finding) - - return security_hub_findings_per_region - - -def verify_security_hub_integration_enabled_per_region( - partition: str, - region: str, - session: session.Session, - aws_account_number: str, -) -> bool: + Class representing a SecurityHub object for managing findings and interactions with AWS Security Hub. + + Attributes: + _session (Session): AWS session object for authentication and communication with AWS services. + _aws_account_id (str): AWS account ID associated with the SecurityHub instance. + _aws_partition (str): AWS partition (e.g., aws, aws-cn, aws-us-gov) where SecurityHub is deployed. + _findings_per_region (dict): Dictionary containing findings per region. + _enabled_regions (dict): Dictionary containing enabled regions with SecurityHub clients. + + Methods: + __init__: Initializes the SecurityHub object with necessary attributes. + filter: Filters findings based on region and status, returning a dictionary with findings per region. + verify_enabled_per_region: Verifies and stores enabled regions with SecurityHub clients. + batch_send_to_security_hub: Sends findings to Security Hub and returns the count of successfully sent findings. + archive_previous_findings: Archives findings that are not present in the current execution. + _send_findings_to_security_hub: Sends findings to AWS Security Hub in batches and returns the count of successfully sent findings. """ - verify_security_hub_integration_enabled_per_region returns True if the Prowler integration is enabled for the given region. Otherwise returns false. - - Args: - partition (str): AWS partition - region (str): AWS region - session (session.Session): AWS session - aws_account_number (str): AWS account number - Returns: - bool: True if the Prowler integration is enabled for the given region. Otherwise returns false. - """ - f"""verify_security_hub_integration_enabled returns True if the {SECURITY_HUB_INTEGRATION_NAME} is enabled for the given region. Otherwise returns false.""" - prowler_integration_enabled = False - - try: - logger.info( - f"Checking if the {SECURITY_HUB_INTEGRATION_NAME} is enabled in the {region} region." - ) - # Check if security hub is enabled in current region - security_hub_client = session.client("securityhub", region_name=region) - security_hub_client.describe_hub() - - # Check if Prowler integration is enabled in Security Hub - security_hub_prowler_integration_arn = f"arn:{partition}:securityhub:{region}:{aws_account_number}:product-subscription/{SECURITY_HUB_INTEGRATION_NAME}" - if security_hub_prowler_integration_arn not in str( - security_hub_client.list_enabled_products_for_import() - ): - logger.warning( - f"Security Hub is enabled in {region} but Prowler integration does not accept findings. More info: https://docs.prowler.cloud/en/latest/tutorials/aws/securityhub/" - ) - else: - prowler_integration_enabled = True - - # Handle all the permissions / configuration errors - except ClientError as client_error: - # Check if Account is subscribed to Security Hub - error_code = client_error.response["Error"]["Code"] - error_message = client_error.response["Error"]["Message"] - if ( - error_code == "InvalidAccessException" - and f"Account {aws_account_number} is not subscribed to AWS Security Hub" - in error_message - ): - logger.warning( - f"{client_error.__class__.__name__} -- [{client_error.__traceback__.tb_lineno}]: {client_error}" + _session: Session + _aws_account_id: str + _aws_partition: str + _findings_per_region: dict[str, list[AWSSecurityFindingFormat]] + _enabled_regions: dict[str, Session] + + def __init__( + self, + aws_session: Session, + aws_account_id: str, + aws_partition: str, + findings: list[AWSSecurityFindingFormat] = [], + status: list[str] = [], + aws_security_hub_available_regions: list[str] = [], + send_only_fails: bool = False, + ) -> "SecurityHub": + self._session = aws_session + self._aws_account_id = aws_account_id + self._aws_partition = aws_partition + + self._enabled_regions = None + self._findings_per_region = None + + if aws_security_hub_available_regions: + self._enabled_regions = self.verify_enabled_per_region( + aws_security_hub_available_regions ) - else: - logger.error( - f"{client_error.__class__.__name__} -- [{client_error.__traceback__.tb_lineno}]: {client_error}" - ) - except Exception as error: - logger.error( - f"{error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]: {error}" - ) - - finally: - return prowler_integration_enabled - - -def batch_send_to_security_hub( - security_hub_findings_per_region: dict, - session: session.Session, -) -> int: - """ - batch_send_to_security_hub sends findings to Security Hub and returns the number of findings that were successfully sent. + if findings and self._enabled_regions: + self._findings_per_region = self.filter(findings, send_only_fails, status) + + def filter( + self, + findings: list[AWSSecurityFindingFormat], + send_only_fails: bool, + status: list[str], + ) -> dict: + """ + Filters the given list of findings based on the provided criteria and returns a dictionary containing findings per region. + + Args: + findings (list[AWSSecurityFindingFormat]): List of findings to filter. + send_only_fails (bool): Flag indicating whether to send only findings with status 'FAILED'. + status (list[str]): List of valid statuses to filter the findings. + + Returns: + dict: A dictionary containing findings per region after applying the filtering criteria. + """ + + findings_per_region = {} + + # Create a key per audited region + for region in self._enabled_regions.keys(): + findings_per_region[region] = [] + + for finding in findings: + # We don't send findings to not enabled regions + if finding.Resources[0].Region not in findings_per_region: + continue - Args: - security_hub_findings_per_region (dict): Dictionary containing the findings per region - session (session.Session): AWS session + if ( + finding.Compliance.Status != "FAILED" + or finding.Compliance.Status == "WARNING" + ) and send_only_fails: + continue - Returns: - int: Number of sent findings - """ + # SecurityHub valid statuses are: PASSED, FAILED, WARNING + if status: + if finding.Compliance.Status == "PASSED" and "PASS" not in status: + continue + if finding.Compliance.Status == "FAILED" and "FAIL" not in status: + continue + # Check muted finding + if finding.Compliance.Status == "WARNING": + continue + + # Get the finding region + # We can do that since the finding always stores just one finding + region = finding.Resources[0].Region + + # Include that finding within their region + findings_per_region[region].append(finding) + + return findings_per_region + + def verify_enabled_per_region( + self, + aws_security_hub_available_regions: list[str], + ) -> dict[str, Session]: + """ + Filters the given list of regions where AWS Security Hub is enabled and returns a dictionary containing the region and their boto3 client if the region and the Prowler integration is enabled. + + Args: + aws_security_hub_available_regions (list[str]): List of AWS regions to check for Security Hub integration. + + Returns: + dict: A dictionary containing enabled regions with SecurityHub clients. + """ + enabled_regions = {} + for region in aws_security_hub_available_regions: + try: + logger.info( + f"Checking if the {SECURITY_HUB_INTEGRATION_NAME} is enabled in the {region} region." + ) + # Check if security hub is enabled in current region + security_hub_client = self._session.client( + "securityhub", region_name=region + ) + security_hub_client.describe_hub() + + # Check if Prowler integration is enabled in Security Hub + security_hub_prowler_integration_arn = f"arn:{self._aws_partition}:securityhub:{region}:{self._aws_account_id}:product-subscription/{SECURITY_HUB_INTEGRATION_NAME}" + if security_hub_prowler_integration_arn not in str( + security_hub_client.list_enabled_products_for_import() + ): + logger.warning( + f"Security Hub is enabled in {region} but Prowler integration does not accept findings. More info: https://docs.prowler.cloud/en/latest/tutorials/aws/securityhub/" + ) + else: + enabled_regions[region] = self._session.client( + "securityhub", region_name=region + ) + + # Handle all the permissions / configuration errors + except ClientError as client_error: + # Check if Account is subscribed to Security Hub + error_code = client_error.response["Error"]["Code"] + error_message = client_error.response["Error"]["Message"] + if ( + error_code == "InvalidAccessException" + and f"Account {self._aws_account_id} is not subscribed to AWS Security Hub" + in error_message + ): + logger.warning( + f"{client_error.__class__.__name__} -- [{client_error.__traceback__.tb_lineno}]: {client_error}" + ) + else: + logger.error( + f"{client_error.__class__.__name__} -- [{client_error.__traceback__.tb_lineno}]: {client_error}" + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]: {error}" + ) + return enabled_regions + + def batch_send_to_security_hub( + self, + ) -> int: + """ + Sends the findings to AWS Security Hub in batches for each region and returns the count of successfully sent findings. + + Returns: + int: Number of successfully sent findings to AWS Security Hub. + """ + success_count = 0 + try: + # Iterate findings by region + for region, findings in self._findings_per_region.items(): + # Send findings to Security Hub + logger.info( + f"Sending {len(findings)} findings to Security Hub in the region {region}" + ) - success_count = 0 - try: - # Iterate findings by region - for region, findings in security_hub_findings_per_region.items(): - # Send findings to Security Hub - logger.info( - f"Sending {len(findings)} findings to Security Hub in the region {region}" - ) + # Convert findings to dict + findings = [finding.dict(exclude_none=True) for finding in findings] + success_count += self._send_findings_in_batches( + findings, + region, + ) - security_hub_client = session.client("securityhub", region_name=region) - # Convert findings to dict - findings = [finding.dict(exclude_none=True) for finding in findings] - success_count += _send_findings_to_security_hub( - findings, region, security_hub_client + except Exception as error: + logger.error( + f"{error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]:{error} in region {region}" ) + return success_count - except Exception as error: - logger.error( - f"{error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]:{error} in region {region}" - ) - return success_count - + def archive_previous_findings(self) -> int: + """ + Checks previous findings in Security Hub to archive them. + + Returns: + int: Number of successfully archived findings. + """ + logger.info("Checking previous findings in Security Hub to archive them.") + success_count = 0 + for region in self._findings_per_region.keys(): + try: + current_findings = self._findings_per_region[region] + # Get current findings IDs + current_findings_ids = [] + for finding in current_findings: + current_findings_ids.append(finding.Id) + # Get findings of that region + findings_filter = { + "ProductName": [{"Value": "Prowler", "Comparison": "EQUALS"}], + "RecordState": [{"Value": "ACTIVE", "Comparison": "EQUALS"}], + "AwsAccountId": [ + {"Value": self._aws_account_id, "Comparison": "EQUALS"} + ], + "Region": [{"Value": region, "Comparison": "EQUALS"}], + } + get_findings_paginator = self._enabled_regions[region].get_paginator( + "get_findings" + ) + findings_to_archive = [] + for page in get_findings_paginator.paginate( + Filters=findings_filter, PaginationConfig={"PageSize": 100} + ): + # Archive findings that have not appear in this execution + for finding in page["Findings"]: + if finding["Id"] not in current_findings_ids: + finding["RecordState"] = "ARCHIVED" + finding["UpdatedAt"] = timestamp_utc.strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) + + findings_to_archive.append(finding) + logger.info(f"Archiving {len(findings_to_archive)} findings.") + + # Send archive findings to SHub + success_count += self._send_findings_in_batches( + findings_to_archive, + region, + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]:{error} in region {region}" + ) + return success_count -# Move previous Security Hub check findings to ARCHIVED (as prowler didn't re-detect them) -def resolve_security_hub_previous_findings( - security_hub_findings_per_region: dict, provider -) -> int: - """ - resolve_security_hub_previous_findings archives all the findings that does not appear in the current execution + def _send_findings_in_batches( + self, findings: list[AWSSecurityFindingFormat], region: str + ) -> int: + """ + Sends the given findings to AWS Security Hub in batches for a specific region and returns the count of successfully sent findings. - Args: - security_hub_findings_per_region (dict): Dictionary containing the findings per region - provider: Provider object + Args: + findings (list[AWSSecurityFindingFormat]): List of findings to send to AWS Security Hub. + region (str): The AWS region where the findings will be sent. - Returns: - int: Number of archived findings - """ - logger.info("Checking previous findings in Security Hub to archive them.") - success_count = 0 - for region in security_hub_findings_per_region.keys(): + Returns: + int: Number of successfully sent findings to AWS Security Hub. + """ + success_count = 0 try: - current_findings = security_hub_findings_per_region[region] - # Get current findings IDs - current_findings_ids = [] - for finding in current_findings: - current_findings_ids.append(finding.Id) - # Get findings of that region - security_hub_client = provider.session.current_session.client( - "securityhub", region_name=region - ) - findings_filter = { - "ProductName": [{"Value": "Prowler", "Comparison": "EQUALS"}], - "RecordState": [{"Value": "ACTIVE", "Comparison": "EQUALS"}], - "AwsAccountId": [ - {"Value": provider.identity.account, "Comparison": "EQUALS"} - ], - "Region": [{"Value": region, "Comparison": "EQUALS"}], - } - get_findings_paginator = security_hub_client.get_paginator("get_findings") - findings_to_archive = [] - for page in get_findings_paginator.paginate( - Filters=findings_filter, PaginationConfig={"PageSize": 100} - ): - # Archive findings that have not appear in this execution - for finding in page["Findings"]: - if finding["Id"] not in current_findings_ids: - finding["RecordState"] = "ARCHIVED" - finding["UpdatedAt"] = timestamp_utc.strftime( - "%Y-%m-%dT%H:%M:%SZ" - ) - - findings_to_archive.append(finding) - logger.info(f"Archiving {len(findings_to_archive)} findings.") - - # Send archive findings to SHub - success_count += _send_findings_to_security_hub( - findings_to_archive, region, security_hub_client - ) + list_chunked = [ + findings[i : i + SECURITY_HUB_MAX_BATCH] + for i in range(0, len(findings), SECURITY_HUB_MAX_BATCH) + ] + for findings in list_chunked: + batch_import = self._enabled_regions[region].batch_import_findings( + Findings=findings + ) + if batch_import["FailedCount"] > 0: + failed_import = batch_import["FailedFindings"][0] + logger.error( + f"Failed to send findings to AWS Security Hub -- {failed_import['ErrorCode']} -- {failed_import['ErrorMessage']}" + ) + success_count += batch_import["SuccessCount"] + return success_count except Exception as error: logger.error( f"{error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]:{error} in region {region}" ) - return success_count - - -def _send_findings_to_security_hub( - findings: list[dict], region: str, security_hub_client -) -> int: - """Private function send_findings_to_security_hub chunks the findings in groups of 100 findings and send them to AWS Security Hub. It returns the number of sent findings. - - Args: - findings (list[dict]): List of findings to send to AWS Security Hub - region (str): AWS region to send the findings - security_hub_client: AWS Security Hub client - - Returns: - int: Number of sent findings - """ - success_count = 0 - try: - list_chunked = [ - findings[i : i + SECURITY_HUB_MAX_BATCH] - for i in range(0, len(findings), SECURITY_HUB_MAX_BATCH) - ] - for findings in list_chunked: - batch_import = security_hub_client.batch_import_findings(Findings=findings) - if batch_import["FailedCount"] > 0: - failed_import = batch_import["FailedFindings"][0] - logger.error( - f"Failed to send findings to AWS Security Hub -- {failed_import['ErrorCode']} -- {failed_import['ErrorMessage']}" - ) - success_count += batch_import["SuccessCount"] - return success_count - except Exception as error: - logger.error( - f"{error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]:{error} in region {region}" - ) - return success_count + return success_count diff --git a/tests/providers/aws/lib/security_hub/security_hub_test.py b/tests/providers/aws/lib/security_hub/security_hub_test.py index 38ea57accc..e80a38e799 100644 --- a/tests/providers/aws/lib/security_hub/security_hub_test.py +++ b/tests/providers/aws/lib/security_hub/security_hub_test.py @@ -1,4 +1,5 @@ -from logging import ERROR, WARNING +import re +from logging import WARNING import botocore from boto3 import session @@ -6,11 +7,7 @@ from mock import patch from prowler.lib.outputs.asff.asff import ASFF -from prowler.providers.aws.lib.security_hub.security_hub import ( - batch_send_to_security_hub, - filter_security_hub_findings_per_region, - verify_security_hub_integration_enabled_per_region, -) +from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub from tests.lib.outputs.fixtures.fixtures import generate_finding_output from tests.providers.aws.utils import ( AWS_ACCOUNT_NUMBER, @@ -41,36 +38,36 @@ def mock_make_api_call(self, operation_name, kwarg): return { "ProductSubscriptions": [ f"arn:aws:securityhub:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:product-subscription/prowler/prowler", + f"arn:aws:securityhub:{AWS_REGION_EU_WEST_2}:{AWS_ACCOUNT_NUMBER}:product-subscription/prowler/prowler", ] } return make_api_call(self, operation_name, kwarg) -def set_mocked_session(region=None): - # Create mock session - return session.Session( - region_name=region, - ) - - class TestSecurityHub: @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) def test_verify_security_hub_integration_enabled_per_region(self): - session = set_mocked_session(AWS_REGION_EU_WEST_1) - assert verify_security_hub_integration_enabled_per_region( - AWS_COMMERCIAL_PARTITION, AWS_REGION_EU_WEST_1, session, AWS_ACCOUNT_NUMBER + security_hub = SecurityHub( + aws_session=session.Session( + region_name=AWS_REGION_EU_WEST_1, + ), + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + aws_security_hub_available_regions=[AWS_REGION_EU_WEST_1], ) + assert security_hub._enabled_regions + assert len(security_hub._enabled_regions) == 1 + assert AWS_REGION_EU_WEST_1 in security_hub._enabled_regions def test_verify_security_hub_integration_enabled_per_region_security_hub_disabled( self, caplog ): caplog.set_level(WARNING) - session = set_mocked_session(AWS_REGION_EU_WEST_1) with patch( - "prowler.providers.aws.lib.security_hub.security_hub.session.Session.client", + "prowler.providers.aws.lib.security_hub.security_hub.Session.client", ) as mock_security_hub: error_message = f"Account {AWS_ACCOUNT_NUMBER} is not subscribed to AWS Security Hub in region {AWS_REGION_EU_WEST_1}" error_code = "InvalidAccessException" @@ -83,38 +80,50 @@ def test_verify_security_hub_integration_enabled_per_region_security_hub_disable operation_name = "DescribeHub" mock_security_hub.side_effect = ClientError(error_response, operation_name) - assert not verify_security_hub_integration_enabled_per_region( - AWS_COMMERCIAL_PARTITION, - AWS_REGION_EU_WEST_1, - session, - AWS_ACCOUNT_NUMBER, - ) - assert caplog.record_tuples == [ - ( - "root", - WARNING, - f"ClientError -- [90]: An error occurred ({error_code}) when calling the {operation_name} operation: {error_message}", + log_pattern = re.compile( + r"ClientError -- \[\d+\]: An error occurred \({error_code}\) when calling the {operation_name} operation: {error_message}".format( + error_code=re.escape(error_code), + operation_name=re.escape(operation_name), + error_message=re.escape(error_message), ) - ] + ) + + security_hub = SecurityHub( + aws_session=session.Session( + region_name=AWS_REGION_EU_WEST_1, + ), + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + aws_security_hub_available_regions=[AWS_REGION_EU_WEST_1], + ) + + assert security_hub._enabled_regions == {} + + assert any( + log_pattern.match(record.message) for record in caplog.records + ), "Expected log message not found" def test_verify_security_hub_integration_enabled_per_region_prowler_not_subscribed( self, caplog ): caplog.set_level(WARNING) - session = set_mocked_session(AWS_REGION_EU_WEST_1) with patch( - "prowler.providers.aws.lib.security_hub.security_hub.session.Session.client", + "prowler.providers.aws.lib.security_hub.security_hub.Session.client", ) as mock_security_hub: mock_security_hub.describe_hub.return_value = None mock_security_hub.list_enabled_products_for_import.return_value = [] - assert not verify_security_hub_integration_enabled_per_region( - AWS_COMMERCIAL_PARTITION, - AWS_REGION_EU_WEST_1, - session, - AWS_ACCOUNT_NUMBER, + security_hub = SecurityHub( + aws_session=session.Session( + region_name=AWS_REGION_EU_WEST_1, + ), + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + aws_security_hub_available_regions=[AWS_REGION_EU_WEST_1], ) + + assert security_hub._enabled_regions == {} assert caplog.record_tuples == [ ( "root", @@ -127,10 +136,9 @@ def test_verify_security_hub_integration_enabled_per_region_another_ClientError( self, caplog ): caplog.set_level(WARNING) - session = set_mocked_session(AWS_REGION_EU_WEST_1) with patch( - "prowler.providers.aws.lib.security_hub.security_hub.session.Session.client", + "prowler.providers.aws.lib.security_hub.security_hub.Session.client", ) as mock_security_hub: error_message = f"Another exception in region {AWS_REGION_EU_WEST_1}" error_code = "AnotherException" @@ -143,148 +151,215 @@ def test_verify_security_hub_integration_enabled_per_region_another_ClientError( operation_name = "DescribeHub" mock_security_hub.side_effect = ClientError(error_response, operation_name) - assert not verify_security_hub_integration_enabled_per_region( - AWS_COMMERCIAL_PARTITION, - AWS_REGION_EU_WEST_1, - session, - AWS_ACCOUNT_NUMBER, - ) - assert caplog.record_tuples == [ - ( - "root", - ERROR, - f"ClientError -- [90]: An error occurred ({error_code}) when calling the {operation_name} operation: {error_message}", + log_pattern = re.compile( + r"ClientError -- \[\d+\]: An error occurred \({error_code}\) when calling the {operation_name} operation: {error_message}".format( + error_code=re.escape(error_code), + operation_name=re.escape(operation_name), + error_message=re.escape(error_message), ) - ] + ) + + security_hub = SecurityHub( + aws_session=session.Session( + region_name=AWS_REGION_EU_WEST_1, + ), + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + aws_security_hub_available_regions=[AWS_REGION_EU_WEST_1], + ) + + assert security_hub._enabled_regions == {} + assert any( + log_pattern.match(record.message) for record in caplog.records + ), "Expected log message not found" def test_verify_security_hub_integration_enabled_per_region_another_Exception( self, caplog ): caplog.set_level(WARNING) - session = set_mocked_session(AWS_REGION_EU_WEST_1) with patch( - "prowler.providers.aws.lib.security_hub.security_hub.session.Session.client", + "prowler.providers.aws.lib.security_hub.security_hub.Session.client", ) as mock_security_hub: error_message = f"Another exception in region {AWS_REGION_EU_WEST_1}" mock_security_hub.side_effect = Exception(error_message) - assert not verify_security_hub_integration_enabled_per_region( - AWS_COMMERCIAL_PARTITION, - AWS_REGION_EU_WEST_1, - session, - AWS_ACCOUNT_NUMBER, + security_hub = SecurityHub( + aws_session=session.Session( + region_name=AWS_REGION_EU_WEST_1, + ), + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + aws_security_hub_available_regions=[AWS_REGION_EU_WEST_1], ) - assert caplog.record_tuples == [ - ( - "root", - ERROR, - f"Exception -- [90]: {error_message}", + + log_pattern = re.compile( + r"Exception -- \[\d+\]: {error_message}".format( + error_message=re.escape(error_message), ) - ] + ) + + assert security_hub._enabled_regions == {} + assert any( + log_pattern.match(record.message) for record in caplog.records + ), "Expected log message not found" + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) def test_filter_security_hub_findings_per_region_enabled_region_all_statuses(self): - enabled_regions = [AWS_REGION_EU_WEST_1] findings = [generate_finding_output(status="PASS", region=AWS_REGION_EU_WEST_1)] asff = ASFF(findings=findings) - assert filter_security_hub_findings_per_region( - asff.data, - False, - [], - enabled_regions, - ) == {AWS_REGION_EU_WEST_1: [asff.data[0]]} + security_hub = SecurityHub( + aws_session=session.Session( + region_name=AWS_REGION_EU_WEST_1, + ), + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + aws_security_hub_available_regions=[AWS_REGION_EU_WEST_1], + findings=asff.data, + ) + + assert security_hub._findings_per_region == { + AWS_REGION_EU_WEST_1: [asff.data[0]] + } + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) def test_filter_security_hub_findings_per_region_all_statuses_MANUAL_finding(self): - enabled_regions = [AWS_REGION_EU_WEST_1] findings = [ generate_finding_output(status="MANUAL", region=AWS_REGION_EU_WEST_1) ] asff = ASFF(findings=findings) - assert filter_security_hub_findings_per_region( - asff.data, - False, - [], - enabled_regions, - ) == {AWS_REGION_EU_WEST_1: []} + security_hub = SecurityHub( + aws_session=session.Session( + region_name=AWS_REGION_EU_WEST_1, + ), + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + aws_security_hub_available_regions=[AWS_REGION_EU_WEST_1], + findings=asff.data, + ) + + assert security_hub._findings_per_region is None + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) def test_filter_security_hub_findings_per_region_disabled_region(self): - enabled_regions = [AWS_REGION_EU_WEST_1] findings = [generate_finding_output(status="PASS", region=AWS_REGION_EU_WEST_2)] asff = ASFF(findings=findings) - assert filter_security_hub_findings_per_region( - asff.data, - False, - [], - enabled_regions, - ) == {AWS_REGION_EU_WEST_1: []} + security_hub = SecurityHub( + aws_session=session.Session( + region_name=AWS_REGION_EU_WEST_1, + ), + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + aws_security_hub_available_regions=[AWS_REGION_EU_WEST_1], + findings=asff.data, + ) + + assert security_hub._findings_per_region == {AWS_REGION_EU_WEST_1: []} + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) def test_filter_security_hub_findings_per_region_PASS_and_FAIL_statuses(self): - enabled_regions = [AWS_REGION_EU_WEST_1] findings = [generate_finding_output(status="PASS", region=AWS_REGION_EU_WEST_1)] asff = ASFF(findings=findings) - assert filter_security_hub_findings_per_region( - asff.data, - False, - ["FAIL"], - enabled_regions, - ) == {AWS_REGION_EU_WEST_1: []} + security_hub = SecurityHub( + aws_session=session.Session( + region_name=AWS_REGION_EU_WEST_1, + ), + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + aws_security_hub_available_regions=[AWS_REGION_EU_WEST_1], + findings=asff.data, + status=["FAIL"], + ) + + assert security_hub._findings_per_region == {AWS_REGION_EU_WEST_1: []} + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) def test_filter_security_hub_findings_per_region_FAIL_and_FAIL_statuses(self): - enabled_regions = [AWS_REGION_EU_WEST_1] findings = [generate_finding_output(status="FAIL", region=AWS_REGION_EU_WEST_1)] asff = ASFF(findings=findings) - assert filter_security_hub_findings_per_region( - asff.data, - False, - ["FAIL"], - enabled_regions, - ) == {AWS_REGION_EU_WEST_1: [asff.data[0]]} + security_hub = SecurityHub( + aws_session=session.Session( + region_name=AWS_REGION_EU_WEST_1, + ), + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + aws_security_hub_available_regions=[AWS_REGION_EU_WEST_1], + findings=asff.data, + status=["FAIL"], + ) + + assert security_hub._findings_per_region == { + AWS_REGION_EU_WEST_1: [asff.data[0]] + } + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) def test_filter_security_hub_findings_per_region_send_sh_only_fails_PASS(self): - enabled_regions = [AWS_REGION_EU_WEST_1] findings = [generate_finding_output(status="PASS", region=AWS_REGION_EU_WEST_1)] asff = ASFF(findings=findings) - assert filter_security_hub_findings_per_region( - asff.data, - True, - [], - enabled_regions, - ) == {AWS_REGION_EU_WEST_1: []} + security_hub = SecurityHub( + aws_session=session.Session( + region_name=AWS_REGION_EU_WEST_1, + ), + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + aws_security_hub_available_regions=[AWS_REGION_EU_WEST_1], + findings=asff.data, + status=[], + send_only_fails=True, + ) + assert security_hub._findings_per_region == {AWS_REGION_EU_WEST_1: []} + + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) def test_filter_security_hub_findings_per_region_send_sh_only_fails_FAIL(self): - enabled_regions = [AWS_REGION_EU_WEST_1] findings = [generate_finding_output(status="FAIL", region=AWS_REGION_EU_WEST_1)] asff = ASFF(findings=findings) - assert filter_security_hub_findings_per_region( - asff.data, - True, - [], - enabled_regions, - ) == {AWS_REGION_EU_WEST_1: [asff.data[0]]} + security_hub = SecurityHub( + aws_session=session.Session( + region_name=AWS_REGION_EU_WEST_1, + ), + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + aws_security_hub_available_regions=[AWS_REGION_EU_WEST_1], + findings=asff.data, + status=[], + send_only_fails=True, + ) + + assert security_hub._findings_per_region == { + AWS_REGION_EU_WEST_1: [asff.data[0]] + } + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) def test_filter_security_hub_findings_per_region_no_audited_regions(self): - enabled_regions = [AWS_REGION_EU_WEST_1] findings = [generate_finding_output(status="PASS", region=AWS_REGION_EU_WEST_1)] asff = ASFF(findings=findings) - assert filter_security_hub_findings_per_region( - asff.data, - False, - [], - enabled_regions, - ) == {AWS_REGION_EU_WEST_1: [asff.data[0]]} + security_hub = SecurityHub( + aws_session=session.Session( + region_name=AWS_REGION_EU_WEST_1, + ), + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + aws_security_hub_available_regions=[], + findings=asff.data, + status=[], + send_only_fails=True, + ) + + assert security_hub._findings_per_region is None + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) def test_filter_security_hub_findings_per_region_muted_fail_with_send_sh_only_fails( self, ): - enabled_regions = [AWS_REGION_EU_WEST_1] findings = [ generate_finding_output( status="FAIL", region=AWS_REGION_EU_WEST_1, muted=True @@ -292,17 +367,24 @@ def test_filter_security_hub_findings_per_region_muted_fail_with_send_sh_only_fa ] asff = ASFF(findings=findings) - assert filter_security_hub_findings_per_region( - asff.data, - True, - [], - enabled_regions, - ) == { + security_hub = SecurityHub( + aws_session=session.Session( + region_name=AWS_REGION_EU_WEST_1, + ), + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + aws_security_hub_available_regions=[AWS_REGION_EU_WEST_1], + findings=asff.data, + status=[], + send_only_fails=True, + ) + + assert security_hub._findings_per_region == { AWS_REGION_EU_WEST_1: [], } + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) def test_filter_security_hub_findings_per_region_muted_fail_with_status_FAIL(self): - enabled_regions = [AWS_REGION_EU_WEST_1] findings = [ generate_finding_output( status="FAIL", region=AWS_REGION_EU_WEST_1, muted=True @@ -310,12 +392,19 @@ def test_filter_security_hub_findings_per_region_muted_fail_with_status_FAIL(sel ] asff = ASFF(findings=findings) - assert filter_security_hub_findings_per_region( - asff.data, - False, - ["FAIL"], - enabled_regions, - ) == { + security_hub = SecurityHub( + aws_session=session.Session( + region_name=AWS_REGION_EU_WEST_1, + ), + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + aws_security_hub_available_regions=[AWS_REGION_EU_WEST_1], + findings=asff.data, + status=["FAIL"], + send_only_fails=True, + ) + + assert security_hub._findings_per_region == { AWS_REGION_EU_WEST_1: [], } @@ -327,19 +416,15 @@ def test_batch_send_to_security_hub_one_finding(self): generate_finding_output(status="FAIL", region=AWS_REGION_EU_WEST_2), ] asff = ASFF(findings=findings) - session = set_mocked_session(AWS_REGION_EU_WEST_1) - security_hub_findings = filter_security_hub_findings_per_region( - asff.data, - False, - [], - enabled_regions, + security_hub = SecurityHub( + aws_session=session.Session( + region_name=AWS_REGION_EU_WEST_1, + ), + aws_account_id=AWS_ACCOUNT_NUMBER, + aws_partition=AWS_COMMERCIAL_PARTITION, + aws_security_hub_available_regions=enabled_regions, + findings=asff.data, ) - assert ( - batch_send_to_security_hub( - security_hub_findings, - session, - ) - == 2 - ) + assert security_hub.batch_send_to_security_hub() == 2