mirror of
https://github.com/ghndrx/prowler.git
synced 2026-02-10 14:55:00 +00:00
fix(outputs): apply -q to security hub (#1637)
Co-authored-by: sergargar <sergio@verica.io>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user