Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(status): Recover status filtering #4572

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/tutorials/misc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
```
Expand Down
1 change: 0 additions & 1 deletion prowler/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
8 changes: 8 additions & 0 deletions prowler/lib/check/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions prowler/lib/outputs/outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 24 additions & 34 deletions prowler/providers/aws/lib/security_hub/security_hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

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.
Expand All @@ -41,7 +41,6 @@
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":
Expand All @@ -50,67 +49,58 @@
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.

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(

Check warning on line 101 in prowler/providers/aws/lib/security_hub/security_hub.py

View check run for this annotation

Codecov / codecov/patch

prowler/providers/aws/lib/security_hub/security_hub.py#L100-L101

Added lines #L100 - L101 were not covered by tests
f"{error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]: {error}"
)
return findings_per_region

def verify_enabled_per_region(
Expand Down
71 changes: 70 additions & 1 deletion tests/lib/check/check_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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"
Expand Down Expand Up @@ -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)

Expand Down
28 changes: 2 additions & 26 deletions tests/providers/aws/lib/security_hub/security_hub_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)]
Expand All @@ -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 == {
Expand All @@ -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,
)

Expand All @@ -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,
)

Expand All @@ -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(
Expand All @@ -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,
)

Expand All @@ -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,
)

Expand Down
10 changes: 7 additions & 3 deletions tests/providers/aws/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,19 +107,21 @@ 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:
# Create default AWS Organization
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
Expand Down Expand Up @@ -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
Expand Down