From 8db86992aa3faafae4206237c1a5092b0f95a736 Mon Sep 17 00:00:00 2001 From: Sergio Garcia <38561120+sergargar@users.noreply.github.com> Date: Mon, 2 Jan 2023 15:56:49 +0100 Subject: [PATCH] fix(outputs): apply -q to security hub (#1637) Co-authored-by: sergargar --- prowler/lib/outputs/outputs.py | 6 +- .../aws/lib/security_hub/security_hub.py | 70 ++++++++----- tests/lib/outputs/outputs_test.py | 98 +++++++++++++++++-- 3 files changed, 140 insertions(+), 34 deletions(-) diff --git a/prowler/lib/outputs/outputs.py b/prowler/lib/outputs/outputs.py index ca553f92..480521b2 100644 --- a/prowler/lib/outputs/outputs.py +++ b/prowler/lib/outputs/outputs.py @@ -113,7 +113,11 @@ def report(check_findings, output_options, audit_info): and finding.status != "INFO" ): send_to_security_hub( - finding.region, finding_output, audit_info.audit_session + output_options.is_quiet, + finding.status, + finding.region, + finding_output, + audit_info.audit_session, ) # Common outputs diff --git a/prowler/providers/aws/lib/security_hub/security_hub.py b/prowler/providers/aws/lib/security_hub/security_hub.py index 62d70b74..8aab8772 100644 --- a/prowler/providers/aws/lib/security_hub/security_hub.py +++ b/prowler/providers/aws/lib/security_hub/security_hub.py @@ -15,40 +15,56 @@ from prowler.providers.aws.lib.audit_info.models import AWS_Audit_Info def send_to_security_hub( - region: str, finding_output: Check_Output_JSON_ASFF, session: session.Session -): + is_quiet: bool, + finding_status: str, + region: str, + finding_output: Check_Output_JSON_ASFF, + session: session.Session, +) -> int: + """ + send_to_security_hub send each finding to Security Hub and return the number of findings that were successfully sent + """ + success_count = 0 try: - logger.info("Sending findings to Security Hub.") - # 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 - if "prowler/prowler" not in str( - security_hub_client.list_enabled_products_for_import() - ): - logger.error( - f"Security Hub is enabled in {region} but Prowler integration does not accept findings. More info: https://github.com/prowler-cloud/prowler/#security-hub-integration" - ) - - # Send finding to Security Hub - batch_import = security_hub_client.batch_import_findings( - Findings=[finding_output.dict()] - ) - if batch_import["FailedCount"] > 0: - failed_import = batch_import["FailedFindings"][0] - logger.error( - f"Failed to send archived findings to AWS Security Hub -- {failed_import['ErrorCode']} -- {failed_import['ErrorMessage']}" - ) + # Check if -q option is set + if not is_quiet or (is_quiet and finding_status == "FAIL"): + logger.info("Sending findings to Security Hub.") + # 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 + if "prowler/prowler" not in str( + security_hub_client.list_enabled_products_for_import() + ): + logger.error( + f"Security Hub is enabled in {region} but Prowler integration does not accept findings. More info: https://github.com/prowler-cloud/prowler/#security-hub-integration" + ) + else: + # Send finding to Security Hub + batch_import = security_hub_client.batch_import_findings( + Findings=[finding_output.dict()] + ) + if batch_import["FailedCount"] > 0: + failed_import = batch_import["FailedFindings"][0] + logger.error( + f"Failed to send archived findings to AWS Security Hub -- {failed_import['ErrorCode']} -- {failed_import['ErrorMessage']}" + ) + success_count = batch_import["SuccessCount"] except Exception as error: - logger.error(f"{error.__class__.__name__} -- {error} in region {region}") + 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( output_directory: str, audit_info: AWS_Audit_Info ) -> list: + """ + resolve_security_hub_previous_findings archives all the findings that does not appear in the current execution + """ logger.info("Checking previous findings in Security Hub to archive them.") # Read current findings from json-asff file with open( @@ -113,4 +129,6 @@ def resolve_security_hub_previous_findings( f"Failed to send archived findings to AWS Security Hub -- {failed_import['ErrorCode']} -- {failed_import['ErrorMessage']}" ) except Exception as error: - logger.error(f"{error.__class__.__name__} -- {error} in region {region}") + logger.error( + f"{error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]:{error} in region {region}" + ) diff --git a/tests/lib/outputs/outputs_test.py b/tests/lib/outputs/outputs_test.py index fa101086..96d31135 100644 --- a/tests/lib/outputs/outputs_test.py +++ b/tests/lib/outputs/outputs_test.py @@ -3,6 +3,7 @@ from os import path, remove from unittest import mock import boto3 +import botocore import pytest from colorama import Fore from moto import mock_s3 @@ -35,16 +36,41 @@ from prowler.lib.outputs.outputs import ( ) from prowler.lib.utils.utils import hash_sha512, open_file from prowler.providers.aws.lib.audit_info.models import AWS_Audit_Info +from prowler.providers.aws.lib.security_hub.security_hub import send_to_security_hub + +AWS_ACCOUNT_ID = "123456789012" + +# Mocking Security Hub Get Findings +make_api_call = botocore.client.BaseClient._make_api_call + + +def mock_make_api_call(self, operation_name, kwarg): + if operation_name == "BatchImportFindings": + return { + "FailedCount": 0, + "SuccessCount": 1, + } + if operation_name == "DescribeHub": + return { + "HubArn": "test-hub", + } + if operation_name == "ListEnabledProductsForImport": + return { + "ProductSubscriptions": [ + "prowler/prowler", + ], + } + return make_api_call(self, operation_name, kwarg) class Test_Outputs: def test_fill_file_descriptors(self): - audited_account = "123456789012" + audited_account = AWS_ACCOUNT_ID output_directory = f"{os.path.dirname(os.path.realpath(__file__))}" audit_info = AWS_Audit_Info( original_session=None, audit_session=None, - audited_account="123456789012", + audited_account=AWS_ACCOUNT_ID, audited_identity_arn="test-arn", audited_user_id="test", audited_partition="aws", @@ -177,7 +203,7 @@ class Test_Outputs: # input_audit_info = AWS_Audit_Info( # original_session=None, # audit_session=None, - # audited_account="123456789012", + # audited_account=AWS_ACCOUNT_ID, # audited_identity_arn="test-arn", # audited_user_id="test", # audited_partition="aws", @@ -206,7 +232,7 @@ class Test_Outputs: # expected.AssessmentStartTime = timestamp_iso # expected.FindingUniqueId = "" # expected.Profile = "default" - # expected.AccountId = "123456789012" + # expected.AccountId = AWS_ACCOUNT_ID # expected.OrganizationsInfo = None # expected.Region = "eu-west-1" # expected.Status = "PASS" @@ -221,7 +247,7 @@ class Test_Outputs: input_audit_info = AWS_Audit_Info( original_session=None, audit_session=None, - audited_account="123456789012", + audited_account=AWS_ACCOUNT_ID, audited_identity_arn="test-arn", audited_user_id="test", audited_partition="aws", @@ -253,7 +279,7 @@ class Test_Outputs: ProviderVersion=prowler_version, ProwlerResourceName="test-resource" ) expected.GeneratorId = "prowler-" + finding.check_metadata.CheckID - expected.AwsAccountId = "123456789012" + expected.AwsAccountId = AWS_ACCOUNT_ID expected.Types = finding.check_metadata.CheckType expected.FirstObservedAt = ( expected.UpdatedAt @@ -290,7 +316,7 @@ class Test_Outputs: input_audit_info = AWS_Audit_Info( original_session=None, audit_session=session, - audited_account="123456789012", + audited_account=AWS_ACCOUNT_ID, audited_identity_arn="test-arn", audited_user_id="test", audited_partition="aws", @@ -382,3 +408,61 @@ class Test_Outputs: assert stats["total_fail"] == 0 assert stats["resources_count"] == 0 assert stats["findings_count"] == 0 + + @mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + def test_send_to_security_hub(self): + # Create mock session + session = boto3.session.Session( + region_name="eu-west-1", + ) + input_audit_info = AWS_Audit_Info( + original_session=None, + audit_session=session, + audited_account=AWS_ACCOUNT_ID, + audited_identity_arn="test-arn", + audited_user_id="test", + audited_partition="aws", + profile="default", + profile_region="eu-west-1", + credentials=None, + assumed_role_info=None, + audited_regions=["eu-west-2", "eu-west-1"], + organizations_metadata=None, + ) + finding = Check_Report( + load_check_metadata( + f"{path.dirname(path.realpath(__file__))}/fixtures/metadata.json" + ).json() + ) + finding.resource_details = "Test resource details" + finding.resource_id = "test-resource" + finding.resource_arn = "test-arn" + finding.region = "eu-west-1" + finding.status = "PASS" + finding.status_extended = "This is a test" + + finding_output = Check_Output_JSON_ASFF() + + fill_json_asff(finding_output, input_audit_info, finding) + + assert ( + send_to_security_hub( + False, + finding.status, + finding.region, + finding_output, + input_audit_info.audit_session, + ) + == 1 + ) + # Setting is_quiet to True + assert ( + send_to_security_hub( + True, + finding.status, + finding.region, + finding_output, + input_audit_info.audit_session, + ) + == 0 + )