fix(status): Recover status filtering (#4572)

Co-authored-by: Sergio <sergio@prowler.com>
This commit is contained in:
Pepe Fagoaga
2024-07-31 16:10:07 +02:00
committed by GitHub
parent 26a5ffaf82
commit 8461257428
8 changed files with 113 additions and 66 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ Execute Prowler in verbose mode (like in Version 2):
prowler <provider> --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 <provider> --status [PASS, FAIL, MANUAL]
```
-1
View File
@@ -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,
)
+8
View File
@@ -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)
+1
View File
@@ -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
@@ -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 @@ class SecurityHub:
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 @@ class SecurityHub:
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 @@ class SecurityHub:
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(
+70 -1
View File
@@ -6,6 +6,7 @@ from argparse import Namespace
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 moto import mock_aws
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.check import (
)
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 @@ class TestCheck:
== '{\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)
@@ -240,7 +240,7 @@ class TestSecurityHub:
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 @@ class TestSecurityHub:
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 @@ class TestSecurityHub:
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 @@ class TestSecurityHub:
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 @@ class TestSecurityHub:
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 @@ class TestSecurityHub:
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 @@ class TestSecurityHub:
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 @@ class TestSecurityHub:
aws_partition=AWS_COMMERCIAL_PARTITION,
aws_security_hub_available_regions=[AWS_REGION_EU_WEST_1],
findings=asff.data,
status=["FAIL"],
send_only_fails=True,
)
+7 -3
View File
@@ -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