diff --git a/docs/tutorials/misc.md b/docs/tutorials/misc.md index 6b640103aec..5423322cc33 100644 --- a/docs/tutorials/misc.md +++ b/docs/tutorials/misc.md @@ -10,7 +10,7 @@ Execute Prowler in verbose mode (like in Version 2): prowler --verbose ``` ## Filter findings by status -Prowler can filter the findings by their status: +Prowler can filter the findings by their status, so you can see only in the CLI and in the reports the findings with a specific status: ```console prowler --status [PASS, FAIL, MANUAL] ``` diff --git a/prowler/__main__.py b/prowler/__main__.py index dcd05c10b67..31d555d0dec 100644 --- a/prowler/__main__.py +++ b/prowler/__main__.py @@ -593,7 +593,6 @@ def prowler(): 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, ) diff --git a/prowler/lib/check/check.py b/prowler/lib/check/check.py index 8bfc65f7ec5..887a775869c 100644 --- a/prowler/lib/check/check.py +++ b/prowler/lib/check/check.py @@ -706,6 +706,14 @@ def execute( check_class, verbose, global_provider.output_options.only_logs ) + # Exclude findings per status + if global_provider.output_options.status: + check_findings = [ + finding + for finding in check_findings + if finding.status in global_provider.output_options.status + ] + # Update Audit Status services_executed.add(service) checks_executed.add(check_name) diff --git a/prowler/lib/outputs/outputs.py b/prowler/lib/outputs/outputs.py index fa44d98bcdf..d6c5fe10220 100644 --- a/prowler/lib/outputs/outputs.py +++ b/prowler/lib/outputs/outputs.py @@ -25,6 +25,7 @@ def stdout_report(finding, color, verbose, status, fix): ) +# TODO: Only pass check_findings, provider.output_options and provider.type def report(check_findings, provider): try: output_options = provider.output_options diff --git a/prowler/providers/aws/lib/security_hub/security_hub.py b/prowler/providers/aws/lib/security_hub/security_hub.py index 646dce0e1a6..45ab46e4c9d 100644 --- a/prowler/providers/aws/lib/security_hub/security_hub.py +++ b/prowler/providers/aws/lib/security_hub/security_hub.py @@ -22,7 +22,7 @@ class SecurityHub: Methods: __init__: Initializes the SecurityHub object with necessary attributes. - filter: Filters findings based on region and status, returning a dictionary with findings per region. + filter: Filters findings based on region, 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. @@ -41,7 +41,6 @@ def __init__( 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": @@ -50,20 +49,19 @@ def __init__( self._aws_partition = aws_partition self._enabled_regions = None - self._findings_per_region = None + self._findings_per_region = {} if aws_security_hub_available_regions: self._enabled_regions = self.verify_enabled_per_region( aws_security_hub_available_regions ) if findings and self._enabled_regions: - self._findings_per_region = self.filter(findings, send_only_fails, status) + self._findings_per_region = self.filter(findings, send_only_fails) 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. @@ -71,46 +69,38 @@ def filter( 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 = {} + try: + # Create a key per audited region + for region in self._enabled_regions.keys(): + findings_per_region[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 - - 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": + for finding in findings: + # We don't send findings to not enabled regions + if finding.Resources[0].Region not in findings_per_region: continue - # Get the finding region - # We can do that since the finding always stores just one finding - region = finding.Resources[0].Region + if ( + finding.Compliance.Status != "FAILED" + or finding.Compliance.Status == "WARNING" + ) and send_only_fails: + continue - # Include that finding within their region - findings_per_region[region].append(finding) + # 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) + except Exception as error: + logger.error( + f"{error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]: {error}" + ) return findings_per_region def verify_enabled_per_region( diff --git a/tests/lib/check/check_test.py b/tests/lib/check/check_test.py index e622960400a..b253249fd9b 100644 --- a/tests/lib/check/check_test.py +++ b/tests/lib/check/check_test.py @@ -6,6 +6,7 @@ from importlib.machinery import FileFinder from logging import DEBUG, ERROR from pkgutil import ModuleInfo +from unittest import mock from boto3 import client from colorama import Fore, Style @@ -15,6 +16,7 @@ from prowler.lib.check.check import ( exclude_checks_to_run, exclude_services_to_run, + execute, list_categories, list_checks_json, list_modules, @@ -29,8 +31,16 @@ ) from prowler.lib.check.models import load_check_metadata from prowler.providers.aws.aws_provider import AwsProvider +from prowler.providers.aws.services.accessanalyzer.accessanalyzer_service import ( + Analyzer, +) from tests.lib.check.fixtures.bulk_checks_metadata import test_bulk_checks_metadata -from tests.providers.aws.utils import AWS_REGION_US_EAST_1 +from tests.providers.aws.utils import ( + AWS_ACCOUNT_ARN, + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) # AWS_ACCOUNT_NUMBER = "123456789012" # AWS_REGION = "us-east-1" @@ -792,6 +802,65 @@ def test_list_checks_json_aws_lambda_and_s3(self): == '{\n "aws": [\n "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled",\n "awslambda_function_no_secrets_in_code",\n "awslambda_function_no_secrets_in_variables",\n "awslambda_function_not_publicly_accessible",\n "awslambda_function_url_cors_policy",\n "awslambda_function_url_public",\n "awslambda_function_using_supported_runtimes"\n ]\n}' ) + def test_execute(self): + accessanalyzer_client = mock.MagicMock + accessanalyzer_client.region = AWS_REGION_US_EAST_1 + accessanalyzer_client.analyzers = [ + Analyzer( + arn=AWS_ACCOUNT_ARN, + name=AWS_ACCOUNT_NUMBER, + status="NOT_AVAILABLE", + tags=[], + type="", + region=AWS_REGION_US_EAST_1, + ) + ] + with mock.patch( + "prowler.providers.aws.services.accessanalyzer.accessanalyzer_service.AccessAnalyzer", + accessanalyzer_client, + ): + findings = execute( + service="accessanalyzer", + check_name="accessanalyzer_enabled", + global_provider=set_mocked_aws_provider( + expected_checks=["accessanalyzer_enabled"] + ), + services_executed={"accessanalyzer"}, + checks_executed={"accessanalyzer_enabled"}, + custom_checks_metadata=None, + ) + assert len(findings) == 1 + + def test_execute_with_filtering_status(self): + accessanalyzer_client = mock.MagicMock + accessanalyzer_client.region = AWS_REGION_US_EAST_1 + accessanalyzer_client.analyzers = [ + Analyzer( + arn=AWS_ACCOUNT_ARN, + name=AWS_ACCOUNT_NUMBER, + status="NOT_AVAILABLE", + tags=[], + type="", + region=AWS_REGION_US_EAST_1, + ) + ] + status = ["PASS"] + with mock.patch( + "prowler.providers.aws.services.accessanalyzer.accessanalyzer_service.AccessAnalyzer", + accessanalyzer_client, + ): + findings = execute( + service="accessanalyzer", + check_name="accessanalyzer_enabled", + global_provider=set_mocked_aws_provider( + status=status, expected_checks=["accessanalyzer_enabled"] + ), + services_executed={"accessanalyzer"}, + checks_executed={"accessanalyzer_enabled"}, + custom_checks_metadata=None, + ) + assert len(findings) == 0 + def test_run_check(self, caplog): caplog.set_level(DEBUG) 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 e80a38e7998..7dace9878cf 100644 --- a/tests/providers/aws/lib/security_hub/security_hub_test.py +++ b/tests/providers/aws/lib/security_hub/security_hub_test.py @@ -240,7 +240,7 @@ def test_filter_security_hub_findings_per_region_all_statuses_MANUAL_finding(sel findings=asff.data, ) - assert security_hub._findings_per_region is None + assert security_hub._findings_per_region == {} @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) def test_filter_security_hub_findings_per_region_disabled_region(self): @@ -259,24 +259,6 @@ def test_filter_security_hub_findings_per_region_disabled_region(self): 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): - findings = [generate_finding_output(status="PASS", region=AWS_REGION_EU_WEST_1)] - asff = ASFF(findings=findings) - - 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): findings = [generate_finding_output(status="FAIL", region=AWS_REGION_EU_WEST_1)] @@ -290,7 +272,6 @@ def test_filter_security_hub_findings_per_region_FAIL_and_FAIL_statuses(self): 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 == { @@ -310,7 +291,6 @@ def test_filter_security_hub_findings_per_region_send_sh_only_fails_PASS(self): aws_partition=AWS_COMMERCIAL_PARTITION, aws_security_hub_available_regions=[AWS_REGION_EU_WEST_1], findings=asff.data, - status=[], send_only_fails=True, ) @@ -329,7 +309,6 @@ def test_filter_security_hub_findings_per_region_send_sh_only_fails_FAIL(self): aws_partition=AWS_COMMERCIAL_PARTITION, aws_security_hub_available_regions=[AWS_REGION_EU_WEST_1], findings=asff.data, - status=[], send_only_fails=True, ) @@ -350,11 +329,10 @@ def test_filter_security_hub_findings_per_region_no_audited_regions(self): 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 + assert security_hub._findings_per_region == {} @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( @@ -375,7 +353,6 @@ def test_filter_security_hub_findings_per_region_muted_fail_with_send_sh_only_fa aws_partition=AWS_COMMERCIAL_PARTITION, aws_security_hub_available_regions=[AWS_REGION_EU_WEST_1], findings=asff.data, - status=[], send_only_fails=True, ) @@ -400,7 +377,6 @@ def test_filter_security_hub_findings_per_region_muted_fail_with_status_FAIL(sel aws_partition=AWS_COMMERCIAL_PARTITION, aws_security_hub_available_regions=[AWS_REGION_EU_WEST_1], findings=asff.data, - status=["FAIL"], send_only_fails=True, ) diff --git a/tests/providers/aws/utils.py b/tests/providers/aws/utils.py index a14b4a47017..9b8045bdb33 100644 --- a/tests/providers/aws/utils.py +++ b/tests/providers/aws/utils.py @@ -107,6 +107,7 @@ def set_mocked_aws_provider( original_session: session.Session = None, enabled_regions: set = None, arguments: Namespace = Namespace(), + status: list[str] = [], create_default_organization: bool = True, ) -> AwsProvider: if create_default_organization: @@ -114,12 +115,13 @@ def set_mocked_aws_provider( create_default_aws_organization() # Default arguments - arguments = set_default_provider_arguments(arguments) + arguments = set_default_provider_arguments(arguments, status) # AWS Provider provider = AwsProvider(arguments) # Output options + provider.output_options = arguments, {} # Mock Session @@ -156,8 +158,10 @@ def set_mocked_aws_provider( return provider -def set_default_provider_arguments(arguments: Namespace) -> Namespace: - arguments.status = [] +def set_default_provider_arguments( + arguments: Namespace, status: list = [] +) -> Namespace: + arguments.status = status arguments.output_formats = [] arguments.output_directory = "" arguments.verbose = False