mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-29 14:54:30 +00:00
chore(CLI): run a Prowler scan (#4182)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
This commit is contained in:
1 parent
31a770848a
commit
63e5318a2c
15 files changed
+868
-611
No files matched your search
+62
@@ -1,3 +1,5 @@
|
||||
from argparse import Namespace
|
||||
|
||||
import typer
|
||||
|
||||
from prowler.config.config import available_compliance_frameworks
|
||||
@@ -16,7 +18,11 @@ from prowler.lib.check.check import (
|
||||
print_services,
|
||||
)
|
||||
from prowler.lib.check.checks_loader import load_checks_to_execute
|
||||
from prowler.lib.check.compliance import update_checks_metadata_with_compliance
|
||||
from prowler.lib.logger import logger, logging_levels, set_logging_config
|
||||
from prowler.lib.outputs.security_hub.security_hub import SecurityHub
|
||||
from prowler.lib.scan.scan import Scan
|
||||
from prowler.providers.common.provider import Provider
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
@@ -83,6 +89,7 @@ def main(
|
||||
log_level: str = typer.Option("INFO", "--log-level", help="Set the Log level"),
|
||||
log_file: str = typer.Option(None, "--log-file", help="Set the Log file"),
|
||||
only_logs: bool = typer.Option(False, "--only-logs", help="Only show logs"),
|
||||
profile: str = typer.Option(None, "--profile", help="The profile to use"),
|
||||
):
|
||||
check_provider(provider)
|
||||
if list_services_bool:
|
||||
@@ -152,6 +159,61 @@ def main(
|
||||
else:
|
||||
set_logging_config("INFO", only_logs=True)
|
||||
logger.info("Only logs are shown")
|
||||
if profile:
|
||||
# Execute Prowler
|
||||
checks_to_execute = ["s3_account_level_public_access_blocks"]
|
||||
# Create the provider
|
||||
args = Namespace
|
||||
args.provider = provider
|
||||
args.profile = profile
|
||||
args.verbose = False
|
||||
args.fixer = False
|
||||
args.only_logs = False
|
||||
args.status = []
|
||||
args.output_formats = []
|
||||
args.output_filename = None
|
||||
args.unix_timestamp = False
|
||||
args.output_directory = None
|
||||
args.shodan = None
|
||||
args.security_hub = False
|
||||
args.send_sh_only_fails = False
|
||||
# args.region = ("eu-west-1")
|
||||
Provider.set_global_provider(args)
|
||||
provider = Provider.get_global_provider()
|
||||
bulk_checks_metadata = bulk_load_checks_metadata(provider.type)
|
||||
bulk_compliance_frameworks = bulk_load_compliance_frameworks(provider.type)
|
||||
bulk_checks_metadata = update_checks_metadata_with_compliance(
|
||||
bulk_compliance_frameworks, bulk_checks_metadata
|
||||
)
|
||||
provider.output_options = (args, bulk_checks_metadata)
|
||||
provider.output_options.bulk_checks_metadata = bulk_checks_metadata
|
||||
scan = Scan(provider, checks_to_execute)
|
||||
custom_checks_metadata = None
|
||||
scan_results = scan.scan(custom_checks_metadata)
|
||||
# Verify where AWS Security Hub is enabled
|
||||
aws_security_enabled_regions = []
|
||||
security_hub_regions = (
|
||||
provider.get_available_aws_service_regions("securityhub")
|
||||
if not provider.identity.audited_regions
|
||||
else provider.identity.audited_regions
|
||||
)
|
||||
security_hub = SecurityHub(provider)
|
||||
for region in security_hub_regions:
|
||||
# Save the regions where AWS Security Hub is enabled
|
||||
if security_hub.verify_security_hub_integration_enabled_per_region(
|
||||
region,
|
||||
):
|
||||
aws_security_enabled_regions.append(region)
|
||||
# Prepare the findings to be sent to Security Hub
|
||||
security_hub_findings_per_region = security_hub.prepare_security_hub_findings(
|
||||
scan_results,
|
||||
aws_security_enabled_regions,
|
||||
)
|
||||
# Send the findings to Security Hub
|
||||
findings_sent_to_security_hub = security_hub.batch_send_to_security_hub(
|
||||
security_hub_findings_per_region
|
||||
)
|
||||
print(findings_sent_to_security_hub)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+29
-25
@@ -40,15 +40,10 @@ from prowler.lib.outputs.compliance.compliance import display_compliance_table
|
||||
from prowler.lib.outputs.html.html import add_html_footer, fill_html_overview_statistics
|
||||
from prowler.lib.outputs.json.json import close_json
|
||||
from prowler.lib.outputs.outputs import extract_findings_statistics
|
||||
from prowler.lib.outputs.slack import send_slack_message
|
||||
from prowler.lib.outputs.security_hub.security_hub import SecurityHub
|
||||
from prowler.lib.outputs.slack.slack import Slack
|
||||
from prowler.lib.outputs.summary_table import display_summary_table
|
||||
from prowler.providers.aws.lib.s3.s3 import send_to_s3_bucket
|
||||
from prowler.providers.aws.lib.security_hub.security_hub import (
|
||||
batch_send_to_security_hub,
|
||||
prepare_security_hub_findings,
|
||||
resolve_security_hub_previous_findings,
|
||||
verify_security_hub_integration_enabled_per_region,
|
||||
)
|
||||
from prowler.providers.common.provider import Provider
|
||||
from prowler.providers.common.quick_inventory import run_provider_quick_inventory
|
||||
|
||||
@@ -248,14 +243,22 @@ def prowler():
|
||||
stats = extract_findings_statistics(findings)
|
||||
|
||||
if args.slack:
|
||||
if "SLACK_API_TOKEN" in environ and "SLACK_CHANNEL_ID" in environ:
|
||||
_ = send_slack_message(
|
||||
environ["SLACK_API_TOKEN"],
|
||||
environ["SLACK_CHANNEL_ID"],
|
||||
stats,
|
||||
global_provider,
|
||||
# TODO: this should be also in a config file
|
||||
if "SLACK_API_TOKEN" in environ and (
|
||||
"SLACK_CHANNEL_NAME" in environ or "SLACK_CHANNEL_ID" in environ
|
||||
):
|
||||
|
||||
token = environ["SLACK_API_TOKEN"]
|
||||
channel = (
|
||||
environ["SLACK_CHANNEL_NAME"]
|
||||
if "SLACK_CHANNEL_NAME" in environ
|
||||
else environ["SLACK_CHANNEL_ID"]
|
||||
)
|
||||
prowler_args = " ".join(sys.argv[1:])
|
||||
slack = Slack(token, channel, global_provider)
|
||||
_ = slack.send(stats, prowler_args)
|
||||
else:
|
||||
# Refactor(CLI)
|
||||
logger.critical(
|
||||
"Slack integration needs SLACK_API_TOKEN and SLACK_CHANNEL_ID environment variables (see more in https://docs.prowler.cloud/en/latest/tutorials/integrations/#slack)."
|
||||
)
|
||||
@@ -312,42 +315,43 @@ def prowler():
|
||||
if not global_provider.identity.audited_regions
|
||||
else global_provider.identity.audited_regions
|
||||
)
|
||||
security_hub = SecurityHub(global_provider)
|
||||
|
||||
for region in security_hub_regions:
|
||||
# Save the regions where AWS Security Hub is enabled
|
||||
if verify_security_hub_integration_enabled_per_region(
|
||||
global_provider.identity.partition,
|
||||
if security_hub.verify_security_hub_integration_enabled_per_region(
|
||||
region,
|
||||
global_provider.session.current_session,
|
||||
global_provider.identity.account,
|
||||
):
|
||||
aws_security_enabled_regions.append(region)
|
||||
|
||||
# Prepare the findings to be sent to Security Hub
|
||||
security_hub_findings_per_region = prepare_security_hub_findings(
|
||||
security_hub_findings_per_region = security_hub.prepare_security_hub_findings(
|
||||
findings,
|
||||
global_provider,
|
||||
global_provider.output_options,
|
||||
aws_security_enabled_regions,
|
||||
)
|
||||
|
||||
# Send the findings to Security Hub
|
||||
findings_sent_to_security_hub = batch_send_to_security_hub(
|
||||
security_hub_findings_per_region, global_provider.session.current_session
|
||||
findings_sent_to_security_hub = security_hub.batch_send_to_security_hub(
|
||||
security_hub_findings_per_region
|
||||
)
|
||||
|
||||
# Refactor(CLI)
|
||||
print(
|
||||
f"{Style.BRIGHT}{Fore.GREEN}\n{findings_sent_to_security_hub} findings sent to AWS Security Hub!{Style.RESET_ALL}"
|
||||
)
|
||||
|
||||
# Resolve previous fails of Security Hub
|
||||
if not args.skip_sh_update:
|
||||
# Refactor(CLI)
|
||||
print(
|
||||
f"{Style.BRIGHT}\nArchiving previous findings in AWS Security Hub, please wait...{Style.RESET_ALL}"
|
||||
)
|
||||
findings_archived_in_security_hub = resolve_security_hub_previous_findings(
|
||||
security_hub_findings_per_region,
|
||||
global_provider,
|
||||
findings_archived_in_security_hub = (
|
||||
security_hub.resolve_security_hub_previous_findings(
|
||||
security_hub_findings_per_region,
|
||||
)
|
||||
)
|
||||
# Refactor(CLI)
|
||||
print(
|
||||
f"{Style.BRIGHT}{Fore.GREEN}\n{findings_archived_in_security_hub} findings archived in AWS Security Hub!{Style.RESET_ALL}"
|
||||
)
|
||||
|
||||
+16
-13
@@ -593,12 +593,17 @@ def execute_checks(
|
||||
service,
|
||||
check_name,
|
||||
global_provider,
|
||||
services_executed,
|
||||
checks_executed,
|
||||
custom_checks_metadata,
|
||||
)
|
||||
all_findings.extend(check_findings)
|
||||
|
||||
# Update Audit Status
|
||||
services_executed.add(service)
|
||||
checks_executed.add(check_name)
|
||||
global_provider.audit_metadata = update_audit_metadata(
|
||||
global_provider.audit_metadata, services_executed, checks_executed
|
||||
)
|
||||
|
||||
# If check does not exists in the provider or is from another provider
|
||||
except ModuleNotFoundError:
|
||||
logger.error(
|
||||
@@ -651,12 +656,19 @@ def execute_checks(
|
||||
service,
|
||||
check_name,
|
||||
global_provider,
|
||||
services_executed,
|
||||
checks_executed,
|
||||
custom_checks_metadata,
|
||||
)
|
||||
all_findings.extend(check_findings)
|
||||
|
||||
# Update Audit Status
|
||||
services_executed.add(service)
|
||||
checks_executed.add(check_name)
|
||||
global_provider.audit_metadata = update_audit_metadata(
|
||||
global_provider.audit_metadata,
|
||||
services_executed,
|
||||
checks_executed,
|
||||
)
|
||||
|
||||
# If check does not exists in the provider or is from another provider
|
||||
except ModuleNotFoundError:
|
||||
# TODO: add more loggin here, we need the original exception -- traceback.print_last()
|
||||
@@ -677,8 +689,6 @@ def execute(
|
||||
service: str,
|
||||
check_name: str,
|
||||
global_provider: Any,
|
||||
services_executed: set,
|
||||
checks_executed: set,
|
||||
custom_checks_metadata: Any,
|
||||
):
|
||||
try:
|
||||
@@ -700,13 +710,6 @@ def execute(
|
||||
# Run check
|
||||
check_findings = run_check(check_class, global_provider.output_options)
|
||||
|
||||
# Update Audit Status
|
||||
services_executed.add(service)
|
||||
checks_executed.add(check_name)
|
||||
global_provider.audit_metadata = update_audit_metadata(
|
||||
global_provider.audit_metadata, services_executed, checks_executed
|
||||
)
|
||||
|
||||
# Mutelist findings
|
||||
if hasattr(global_provider, "mutelist") and global_provider.mutelist:
|
||||
check_findings = mutelist_findings(
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,235 @@
|
||||
from boto3.session import Session
|
||||
from botocore.client import ClientError
|
||||
|
||||
from prowler.config.config import timestamp_utc
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.json_asff.json_asff import fill_json_asff
|
||||
from prowler.providers.aws.aws_provider import AwsProvider
|
||||
|
||||
SECURITY_HUB_INTEGRATION_NAME = "prowler/prowler"
|
||||
SECURITY_HUB_MAX_BATCH = 100
|
||||
|
||||
|
||||
class SecurityHub:
|
||||
_session: Session
|
||||
_provider: AwsProvider
|
||||
_account: str
|
||||
_partition: str
|
||||
|
||||
def __init__(self, provider: AwsProvider) -> "SecurityHub":
|
||||
self._provider = provider
|
||||
self._session = provider.session.current_session
|
||||
self._account = provider.identity.account
|
||||
self._partition = provider.identity.partition
|
||||
|
||||
@property
|
||||
def findings_per_region(self):
|
||||
return self._findings_per_region
|
||||
|
||||
def prepare_security_hub_findings(
|
||||
self, findings: list, enabled_regions: list
|
||||
) -> dict:
|
||||
security_hub_findings_per_region = {}
|
||||
|
||||
# Create a key per audited region
|
||||
for region in enabled_regions:
|
||||
security_hub_findings_per_region[region] = []
|
||||
|
||||
for finding in findings:
|
||||
# We don't send the MANUAL findings to AWS Security Hub
|
||||
if finding.status == "MANUAL":
|
||||
continue
|
||||
|
||||
# We don't send findings to not enabled regions
|
||||
if finding.region not in enabled_regions:
|
||||
continue
|
||||
|
||||
if (
|
||||
finding.status != "FAIL" or finding.muted
|
||||
) and self._provider.output_options.send_sh_only_fails:
|
||||
continue
|
||||
|
||||
if self._provider.output_options.status:
|
||||
if finding.status not in self._provider.output_options.status:
|
||||
continue
|
||||
|
||||
if finding.muted:
|
||||
continue
|
||||
|
||||
# Get the finding region
|
||||
region = finding.region
|
||||
|
||||
# Format the finding in the JSON ASFF format
|
||||
finding_json_asff = fill_json_asff(self._provider, finding)
|
||||
|
||||
# Include that finding within their region in the JSON format
|
||||
security_hub_findings_per_region[region].append(
|
||||
finding_json_asff.dict(exclude_none=True)
|
||||
)
|
||||
|
||||
return security_hub_findings_per_region
|
||||
|
||||
def verify_security_hub_integration_enabled_per_region(
|
||||
self,
|
||||
region: str,
|
||||
) -> bool:
|
||||
f"""
|
||||
verify_security_hub_integration_enabled returns True if the {SECURITY_HUB_INTEGRATION_NAME} is enabled for the given region. Otherwise returns false.
|
||||
"""
|
||||
prowler_integration_enabled = False
|
||||
|
||||
try:
|
||||
logger.info(
|
||||
f"Checking if the {SECURITY_HUB_INTEGRATION_NAME} is enabled in the {region} region."
|
||||
)
|
||||
# Check if security hub is enabled in current region
|
||||
security_hub_client = self._session.client(
|
||||
"securityhub", region_name=region
|
||||
)
|
||||
security_hub_client.describe_hub()
|
||||
|
||||
# Check if Prowler integration is enabled in Security Hub
|
||||
security_hub_prowler_integration_arn = f"arn:{self._partition}:securityhub:{region}:{self._account}:product-subscription/{SECURITY_HUB_INTEGRATION_NAME}"
|
||||
if security_hub_prowler_integration_arn not in str(
|
||||
security_hub_client.list_enabled_products_for_import()
|
||||
):
|
||||
logger.warning(
|
||||
f"Security Hub is enabled in {region} but Prowler integration does not accept findings. More info: https://docs.prowler.cloud/en/latest/tutorials/aws/securityhub/"
|
||||
)
|
||||
else:
|
||||
prowler_integration_enabled = True
|
||||
|
||||
# Handle all the permissions / configuration errors
|
||||
except ClientError as client_error:
|
||||
# Check if Account is subscribed to Security Hub
|
||||
error_code = client_error.response["Error"]["Code"]
|
||||
error_message = client_error.response["Error"]["Message"]
|
||||
if (
|
||||
error_code == "InvalidAccessException"
|
||||
and f"Account {self._account} is not subscribed to AWS Security Hub"
|
||||
in error_message
|
||||
):
|
||||
logger.warning(
|
||||
f"{client_error.__class__.__name__} -- [{client_error.__traceback__.tb_lineno}]: {client_error}"
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f"{client_error.__class__.__name__} -- [{client_error.__traceback__.tb_lineno}]: {client_error}"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
finally:
|
||||
return prowler_integration_enabled
|
||||
|
||||
def batch_send_to_security_hub(
|
||||
self,
|
||||
security_hub_findings_per_region: dict,
|
||||
) -> int:
|
||||
"""
|
||||
send_to_security_hub sends findings to Security Hub and returns the number of findings that were successfully sent.
|
||||
"""
|
||||
|
||||
success_count = 0
|
||||
try:
|
||||
# Iterate findings by region
|
||||
for region, findings in security_hub_findings_per_region.items():
|
||||
# Send findings to Security Hub
|
||||
logger.info(f"Sending findings to Security Hub in the region {region}")
|
||||
|
||||
security_hub_client = self._session.client(
|
||||
"securityhub", region_name=region
|
||||
)
|
||||
|
||||
success_count += self.__send_findings_to_security_hub__(
|
||||
findings, region, security_hub_client
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
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(
|
||||
self, security_hub_findings_per_region: dict
|
||||
) -> 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.")
|
||||
success_count = 0
|
||||
for region in security_hub_findings_per_region.keys():
|
||||
try:
|
||||
current_findings = security_hub_findings_per_region[region]
|
||||
# Get current findings IDs
|
||||
current_findings_ids = []
|
||||
for finding in current_findings:
|
||||
current_findings_ids.append(finding["Id"])
|
||||
# Get findings of that region
|
||||
security_hub_client = self._session.client(
|
||||
"securityhub", region_name=region
|
||||
)
|
||||
findings_filter = {
|
||||
"ProductName": [{"Value": "Prowler", "Comparison": "EQUALS"}],
|
||||
"RecordState": [{"Value": "ACTIVE", "Comparison": "EQUALS"}],
|
||||
"AwsAccountId": [{"Value": self._account, "Comparison": "EQUALS"}],
|
||||
"Region": [{"Value": region, "Comparison": "EQUALS"}],
|
||||
}
|
||||
get_findings_paginator = security_hub_client.get_paginator(
|
||||
"get_findings"
|
||||
)
|
||||
findings_to_archive = []
|
||||
for page in get_findings_paginator.paginate(Filters=findings_filter):
|
||||
# Archive findings that have not appear in this execution
|
||||
for finding in page["Findings"]:
|
||||
if finding["Id"] not in current_findings_ids:
|
||||
finding["RecordState"] = "ARCHIVED"
|
||||
finding["UpdatedAt"] = timestamp_utc.strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ"
|
||||
)
|
||||
|
||||
findings_to_archive.append(finding)
|
||||
logger.info(f"Archiving {len(findings_to_archive)} findings.")
|
||||
|
||||
# Send archive findings to SHub
|
||||
success_count += self.__send_findings_to_security_hub__(
|
||||
findings_to_archive, region, security_hub_client
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]:{error} in region {region}"
|
||||
)
|
||||
return success_count
|
||||
|
||||
def __send_findings_to_security_hub__(
|
||||
self, findings: list[dict], region: str, security_hub_client
|
||||
):
|
||||
"""Private function send_findings_to_security_hub chunks the findings in groups of 100 findings and send them to AWS Security Hub. It returns the number of sent findings."""
|
||||
success_count = 0
|
||||
try:
|
||||
list_chunked = [
|
||||
findings[i : i + SECURITY_HUB_MAX_BATCH]
|
||||
for i in range(0, len(findings), SECURITY_HUB_MAX_BATCH)
|
||||
]
|
||||
|
||||
for findings in list_chunked:
|
||||
batch_import = security_hub_client.batch_import_findings(
|
||||
Findings=findings
|
||||
)
|
||||
if batch_import["FailedCount"] > 0:
|
||||
failed_import = batch_import["FailedFindings"][0]
|
||||
logger.error(
|
||||
f"Failed to send 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.__traceback__.tb_lineno}]:{error} in region {region}"
|
||||
)
|
||||
finally:
|
||||
return success_count
|
||||
@@ -1,156 +0,0 @@
|
||||
import sys
|
||||
|
||||
from slack_sdk import WebClient
|
||||
|
||||
from prowler.config.config import aws_logo, azure_logo, gcp_logo, square_logo_img
|
||||
from prowler.lib.logger import logger
|
||||
|
||||
|
||||
def send_slack_message(token, channel, stats, provider):
|
||||
try:
|
||||
client = WebClient(token=token)
|
||||
identity, logo = create_message_identity(provider)
|
||||
response = client.chat_postMessage(
|
||||
username="Prowler",
|
||||
icon_url=square_logo_img,
|
||||
channel=f"#{channel}",
|
||||
blocks=create_message_blocks(identity, logo, stats),
|
||||
)
|
||||
return response
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
# TODO: move this to each provider
|
||||
def create_message_identity(provider):
|
||||
"""
|
||||
Create a Slack message identity based on the provider type.
|
||||
|
||||
Parameters:
|
||||
- provider (Provider): The Provider (e.g. "AwsProvider", "GcpProvider", "AzureProvide").
|
||||
|
||||
Returns:
|
||||
- identity (str): The message identity based on the provider type.
|
||||
- logo (str): The logo URL associated with the provider type.
|
||||
"""
|
||||
try:
|
||||
identity = ""
|
||||
logo = aws_logo
|
||||
if provider.type == "aws":
|
||||
identity = f"AWS Account *{provider.identity.account}*"
|
||||
elif provider.type == "gcp":
|
||||
identity = f"GCP Projects *{', '.join(provider.project_ids)}*"
|
||||
logo = gcp_logo
|
||||
elif provider.type == "azure":
|
||||
printed_subscriptions = []
|
||||
for key, value in provider.identity.subscriptions.items():
|
||||
intermediate = f"- *{key}: {value}*\n"
|
||||
printed_subscriptions.append(intermediate)
|
||||
identity = f"Azure Subscriptions:\n{''.join(printed_subscriptions)}"
|
||||
logo = azure_logo
|
||||
return identity, logo
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
def create_title(identity, stats):
|
||||
try:
|
||||
title = f"Hey there 👋 \n I'm *Prowler*, _the handy multi-cloud security tool_ :cloud::key:\n\n I have just finished the security assessment on your {identity} with a total of *{stats['findings_count']}* findings."
|
||||
return title
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
|
||||
def create_message_blocks(identity, logo, stats):
|
||||
try:
|
||||
blocks = [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": create_title(identity, stats),
|
||||
},
|
||||
"accessory": {
|
||||
"type": "image",
|
||||
"image_url": logo,
|
||||
"alt_text": "Provider Logo",
|
||||
},
|
||||
},
|
||||
{"type": "divider"},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": f"\n:white_check_mark: *{stats['total_pass']} Passed findings* ({round(stats['total_pass'] / stats['findings_count'] * 100 , 2)}%)\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": f"\n:x: *{stats['total_fail']} Failed findings* ({round(stats['total_fail'] / stats['findings_count'] * 100 , 2)}%)\n ",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": f"\n:bar_chart: *{stats['resources_count']} Scanned Resources*\n",
|
||||
},
|
||||
},
|
||||
{"type": "divider"},
|
||||
{
|
||||
"type": "context",
|
||||
"elements": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": f"Used parameters: `prowler {' '.join(sys.argv[1:])} `",
|
||||
}
|
||||
],
|
||||
},
|
||||
{"type": "divider"},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {"type": "mrkdwn", "text": "Join our Slack Community!"},
|
||||
"accessory": {
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": "Prowler :slack:"},
|
||||
"url": "https://join.slack.com/t/prowler-workspace/shared_invite/zt-1hix76xsl-2uq222JIXrC7Q8It~9ZNog",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "Feel free to contact us in our repo",
|
||||
},
|
||||
"accessory": {
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": "Prowler :github:"},
|
||||
"url": "https://github.com/prowler-cloud/prowler",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "See all the things you can do with ProwlerPro",
|
||||
},
|
||||
"accessory": {
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": "Prowler Pro"},
|
||||
"url": "https://prowler.pro",
|
||||
},
|
||||
},
|
||||
]
|
||||
return blocks
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
Whitespace-only changes.
@@ -0,0 +1,206 @@
|
||||
from typing import Any
|
||||
|
||||
from slack_sdk import WebClient
|
||||
from slack_sdk.web.base_client import SlackResponse
|
||||
|
||||
from prowler.config.config import aws_logo, azure_logo, gcp_logo, square_logo_img
|
||||
from prowler.lib.logger import logger
|
||||
|
||||
|
||||
class Slack:
|
||||
_provider: Any
|
||||
_token: str
|
||||
_channel: str
|
||||
|
||||
def __init__(self, token: str, channel: str, provider: Any) -> "Slack":
|
||||
self._token = token
|
||||
self._channel = channel
|
||||
self._provider = provider
|
||||
|
||||
@property
|
||||
def token(self):
|
||||
return self._token
|
||||
|
||||
@property
|
||||
def channel(self):
|
||||
return self._channel
|
||||
|
||||
def send(self, stats: dict, args: str) -> SlackResponse:
|
||||
"""
|
||||
Sends the findings to Slack.
|
||||
|
||||
Args:
|
||||
stats (dict): A dictionary containing audit statistics.
|
||||
args (str): Command line arguments used for the audit.
|
||||
|
||||
Returns:
|
||||
SlackResponse: Slack response if successful, error object if an exception occurs.
|
||||
"""
|
||||
try:
|
||||
client = WebClient(token=self.token)
|
||||
identity, logo = self.__create_message_identity__(self._provider)
|
||||
response = client.chat_postMessage(
|
||||
username="Prowler",
|
||||
icon_url=square_logo_img,
|
||||
channel=f"#{self.channel}",
|
||||
blocks=self.__create_message_blocks__(identity, logo, stats, args),
|
||||
)
|
||||
return response
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
return error
|
||||
|
||||
def __create_message_identity__(self, provider: Any):
|
||||
"""
|
||||
Create a Slack message identity based on the provider type.
|
||||
|
||||
Parameters:
|
||||
- provider (Provider): The Provider (e.g. "AwsProvider", "GcpProvider", "AzureProvide").
|
||||
|
||||
Returns:
|
||||
- identity (str): The message identity based on the provider type.
|
||||
- logo (str): The logo URL associated with the provider type.
|
||||
"""
|
||||
|
||||
# TODO: support kubernetes
|
||||
try:
|
||||
identity = ""
|
||||
logo = aws_logo
|
||||
if provider.type == "aws":
|
||||
identity = f"AWS Account *{provider.identity.account}*"
|
||||
elif provider.type == "gcp":
|
||||
identity = f"GCP Projects *{', '.join(provider.project_ids)}*"
|
||||
logo = gcp_logo
|
||||
elif provider.type == "azure":
|
||||
printed_subscriptions = []
|
||||
for key, value in provider.identity.subscriptions.items():
|
||||
intermediate = f"- *{key}: {value}*\n"
|
||||
printed_subscriptions.append(intermediate)
|
||||
identity = f"Azure Subscriptions:\n{''.join(printed_subscriptions)}"
|
||||
logo = azure_logo
|
||||
return identity, logo
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def __create_message_blocks__(self, identity, logo, stats, args) -> list:
|
||||
"""
|
||||
Create the Slack message blocks.
|
||||
|
||||
Args:
|
||||
identity: message identity.
|
||||
logo: logo URL.
|
||||
stats: audit statistics.
|
||||
args: command line arguments used.
|
||||
|
||||
Returns:
|
||||
list: list of Slack message blocks.
|
||||
"""
|
||||
try:
|
||||
blocks = [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": self.__create_title__(identity, stats),
|
||||
},
|
||||
"accessory": {
|
||||
"type": "image",
|
||||
"image_url": logo,
|
||||
"alt_text": "Provider Logo",
|
||||
},
|
||||
},
|
||||
{"type": "divider"},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": f"\n:white_check_mark: *{stats['total_pass']} Passed findings* ({round(stats['total_pass'] / stats['findings_count'] * 100 , 2)}%)\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": f"\n:x: *{stats['total_fail']} Failed findings* ({round(stats['total_fail'] / stats['findings_count'] * 100 , 2)}%)\n ",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": f"\n:bar_chart: *{stats['resources_count']} Scanned Resources*\n",
|
||||
},
|
||||
},
|
||||
{"type": "divider"},
|
||||
{
|
||||
"type": "context",
|
||||
"elements": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": f"Used parameters: `prowler {args}`",
|
||||
}
|
||||
],
|
||||
},
|
||||
{"type": "divider"},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {"type": "mrkdwn", "text": "Join our Slack Community!"},
|
||||
"accessory": {
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": "Prowler :slack:"},
|
||||
"url": "https://join.slack.com/t/prowler-workspace/shared_invite/zt-1hix76xsl-2uq222JIXrC7Q8It~9ZNog",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "Feel free to contact us in our repo",
|
||||
},
|
||||
"accessory": {
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": "Prowler :github:"},
|
||||
"url": "https://github.com/prowler-cloud/prowler",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "See all the things you can do with ProwlerPro",
|
||||
},
|
||||
"accessory": {
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": "Prowler Pro"},
|
||||
"url": "https://prowler.pro",
|
||||
},
|
||||
},
|
||||
]
|
||||
return blocks
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
def __create_title__(self, identity, stats) -> str:
|
||||
"""
|
||||
Create the Slack message title.
|
||||
|
||||
Args:
|
||||
identity: message identity.
|
||||
stats: audit statistics.
|
||||
|
||||
Returns:
|
||||
str: Slack message title.
|
||||
"""
|
||||
try:
|
||||
title = f"Hey there 👋 \n I'm *Prowler*, _the handy multi-cloud security tool_ :cloud::key:\n\n I have just finished the security assessment on your {identity} with a total of *{stats['findings_count']}* findings."
|
||||
return title
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
+138
-48
@@ -1,60 +1,150 @@
|
||||
from typing import Any
|
||||
|
||||
from prowler.lib.check.check import execute
|
||||
from prowler.lib.check.check import execute, update_audit_metadata
|
||||
from prowler.lib.check.models import Check_Report
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.providers.common.models import Audit_Metadata
|
||||
from prowler.providers.common.provider import Provider
|
||||
|
||||
|
||||
def scan(
|
||||
checks_to_execute: list,
|
||||
global_provider: Any,
|
||||
custom_checks_metadata: Any,
|
||||
) -> list[Check_Report]:
|
||||
try:
|
||||
# List to store all the check's findings
|
||||
all_findings = []
|
||||
# Services and checks executed for the Audit Status
|
||||
services_executed = set()
|
||||
checks_executed = set()
|
||||
class Scan:
|
||||
# Maybe not needed
|
||||
_provider: Provider
|
||||
# Refactor(Core): This should replace the Audit_Metadata
|
||||
_number_of_checks_to_execute: int = 0
|
||||
_number_of_checks_completed: int = 0
|
||||
# TODO: these should hold a list of Checks()
|
||||
_checks_to_execute: set[str]
|
||||
_service_checks_to_execute: dict[str, set[str]]
|
||||
_service_checks_completed: dict[str, set[str]]
|
||||
_progress: float = 0.0
|
||||
_findings: list = []
|
||||
|
||||
# Initialize the Audit Metadata
|
||||
# TODO: this should be done in the provider class
|
||||
# Refactor(Core): Audit manager?
|
||||
global_provider.audit_metadata = Audit_Metadata(
|
||||
services_scanned=0,
|
||||
expected_checks=checks_to_execute,
|
||||
completed_checks=0,
|
||||
audit_progress=0,
|
||||
)
|
||||
def __init__(self, provider, checks_to_execute):
|
||||
self._provider = provider
|
||||
|
||||
for check_name in checks_to_execute:
|
||||
try:
|
||||
# Recover service from check name
|
||||
service = check_name.split("_")[0]
|
||||
self._number_of_checks_to_execute = len(checks_to_execute)
|
||||
|
||||
check_findings = execute(
|
||||
service,
|
||||
check_name,
|
||||
global_provider,
|
||||
services_executed,
|
||||
checks_executed,
|
||||
custom_checks_metadata,
|
||||
)
|
||||
all_findings.extend(check_findings)
|
||||
service_checks_to_execute = dict()
|
||||
service_checks_completed = dict()
|
||||
|
||||
# If check does not exists in the provider or is from another provider
|
||||
except ModuleNotFoundError:
|
||||
logger.error(
|
||||
f"Check '{check_name}' was not found for the {global_provider.type.upper()} provider"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{check_name} - {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{check_name} - {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
for check in checks_to_execute:
|
||||
# check -> accessanalyzer_enabled
|
||||
# service -> accessanalyzer
|
||||
service = get_service_name_from_check_name(check)
|
||||
if service not in service_checks_to_execute:
|
||||
service_checks_to_execute[service] = set()
|
||||
service_checks_to_execute[service].add(check)
|
||||
|
||||
return all_findings
|
||||
self._service_checks_to_execute = service_checks_to_execute
|
||||
self._service_checks_completed = service_checks_completed
|
||||
self._checks_to_execute = checks_to_execute
|
||||
|
||||
@property
|
||||
def checks_to_execute(self) -> set[str]:
|
||||
return self._checks_to_execute
|
||||
|
||||
@property
|
||||
def service_checks_to_execute(self) -> dict[str, set[str]]:
|
||||
return self._service_checks_to_execute
|
||||
|
||||
@property
|
||||
def service_checks_completed(self) -> dict[str, set[str]]:
|
||||
return self._service_checks_completed
|
||||
|
||||
@property
|
||||
def provider(self) -> Provider:
|
||||
return self._provider
|
||||
|
||||
@property
|
||||
def progress(self) -> float:
|
||||
return self._number_of_checks_completed / self._number_of_checks_to_execute
|
||||
|
||||
@property
|
||||
def findings(self) -> list:
|
||||
return self._findings
|
||||
|
||||
def scan(
|
||||
self,
|
||||
custom_checks_metadata: Any,
|
||||
) -> list[Check_Report]:
|
||||
try:
|
||||
checks_to_execute = self.checks_to_execute
|
||||
# Initialize the Audit Metadata
|
||||
# TODO: this should be done in the provider class
|
||||
# Refactor(Core): Audit manager?
|
||||
self._provider.audit_metadata = Audit_Metadata(
|
||||
services_scanned=0, # Refactor(Core): This shouldn't be nee
|
||||
expected_checks=checks_to_execute,
|
||||
completed_checks=0,
|
||||
audit_progress=0,
|
||||
)
|
||||
|
||||
for check_name in checks_to_execute:
|
||||
try:
|
||||
# Recover service from check name
|
||||
service = get_service_name_from_check_name(check_name)
|
||||
|
||||
# Execute the check
|
||||
check_findings = execute(
|
||||
service,
|
||||
check_name,
|
||||
self._provider,
|
||||
custom_checks_metadata,
|
||||
)
|
||||
# Store findings
|
||||
self._findings.extend(check_findings)
|
||||
|
||||
# Remove the executed check
|
||||
self._service_checks_to_execute[service].remove(check_name)
|
||||
if len(self._service_checks_to_execute[service]) == 0:
|
||||
self._service_checks_to_execute.pop(service, None)
|
||||
# Add the completed check
|
||||
if service not in self._service_checks_completed:
|
||||
self._service_checks_completed[service] = set()
|
||||
self._service_checks_completed[service].add(check_name)
|
||||
self._number_of_checks_completed += 1
|
||||
|
||||
# This should be done just once all the service's checks are completed
|
||||
# This metadata needs to get to the services not within the provider
|
||||
# since it is present in the Scan class
|
||||
self._provider.audit_metadata = update_audit_metadata(
|
||||
self._provider.audit_metadata,
|
||||
self.get_completed_services(),
|
||||
self.get_completed_checks(),
|
||||
)
|
||||
|
||||
# If check does not exists in the provider or is from another provider
|
||||
except ModuleNotFoundError:
|
||||
logger.error(
|
||||
f"Check '{check_name}' was not found for the {self._provider.type.upper()} provider"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{check_name} - {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{check_name} - {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
return self._findings
|
||||
|
||||
def get_completed_services(self):
|
||||
return self._service_checks_completed.keys()
|
||||
|
||||
def get_completed_checks(self):
|
||||
completed_checks = set()
|
||||
for checks in self._service_checks_completed.values():
|
||||
completed_checks.update(checks)
|
||||
return completed_checks
|
||||
|
||||
|
||||
def get_service_name_from_check_name(check_name: str) -> str:
|
||||
"""
|
||||
get_service_name_from_check_name returns the service name for a given check name.
|
||||
|
||||
Example:
|
||||
get_service_name_from_check_name("ec2_instance_public") -> "ec2"
|
||||
"""
|
||||
return check_name.split("_")[0]
|
||||
@@ -1,216 +0,0 @@
|
||||
from boto3 import session
|
||||
from botocore.client import ClientError
|
||||
|
||||
from prowler.config.config import timestamp_utc
|
||||
from prowler.lib.logger import logger
|
||||
from prowler.lib.outputs.json_asff.json_asff import fill_json_asff
|
||||
|
||||
SECURITY_HUB_INTEGRATION_NAME = "prowler/prowler"
|
||||
SECURITY_HUB_MAX_BATCH = 100
|
||||
|
||||
|
||||
def prepare_security_hub_findings(
|
||||
findings: list, provider, output_options, enabled_regions: list
|
||||
) -> dict:
|
||||
security_hub_findings_per_region = {}
|
||||
|
||||
# Create a key per audited region
|
||||
for region in enabled_regions:
|
||||
security_hub_findings_per_region[region] = []
|
||||
|
||||
for finding in findings:
|
||||
# We don't send the MANUAL findings to AWS Security Hub
|
||||
if finding.status == "MANUAL":
|
||||
continue
|
||||
|
||||
# We don't send findings to not enabled regions
|
||||
if finding.region not in enabled_regions:
|
||||
continue
|
||||
|
||||
if (
|
||||
finding.status != "FAIL" or finding.muted
|
||||
) and output_options.send_sh_only_fails:
|
||||
continue
|
||||
|
||||
if output_options.status:
|
||||
if finding.status not in output_options.status:
|
||||
continue
|
||||
|
||||
if finding.muted:
|
||||
continue
|
||||
|
||||
# Get the finding region
|
||||
region = finding.region
|
||||
|
||||
# Format the finding in the JSON ASFF format
|
||||
finding_json_asff = fill_json_asff(provider, finding)
|
||||
|
||||
# Include that finding within their region in the JSON format
|
||||
security_hub_findings_per_region[region].append(
|
||||
finding_json_asff.dict(exclude_none=True)
|
||||
)
|
||||
|
||||
return security_hub_findings_per_region
|
||||
|
||||
|
||||
def verify_security_hub_integration_enabled_per_region(
|
||||
partition: str,
|
||||
region: str,
|
||||
session: session.Session,
|
||||
aws_account_number: str,
|
||||
) -> bool:
|
||||
f"""verify_security_hub_integration_enabled returns True if the {SECURITY_HUB_INTEGRATION_NAME} is enabled for the given region. Otherwise returns false."""
|
||||
prowler_integration_enabled = False
|
||||
|
||||
try:
|
||||
logger.info(
|
||||
f"Checking if the {SECURITY_HUB_INTEGRATION_NAME} is enabled in the {region} region."
|
||||
)
|
||||
# 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
|
||||
security_hub_prowler_integration_arn = f"arn:{partition}:securityhub:{region}:{aws_account_number}:product-subscription/{SECURITY_HUB_INTEGRATION_NAME}"
|
||||
if security_hub_prowler_integration_arn not in str(
|
||||
security_hub_client.list_enabled_products_for_import()
|
||||
):
|
||||
logger.warning(
|
||||
f"Security Hub is enabled in {region} but Prowler integration does not accept findings. More info: https://docs.prowler.cloud/en/latest/tutorials/aws/securityhub/"
|
||||
)
|
||||
else:
|
||||
prowler_integration_enabled = True
|
||||
|
||||
# Handle all the permissions / configuration errors
|
||||
except ClientError as client_error:
|
||||
# Check if Account is subscribed to Security Hub
|
||||
error_code = client_error.response["Error"]["Code"]
|
||||
error_message = client_error.response["Error"]["Message"]
|
||||
if (
|
||||
error_code == "InvalidAccessException"
|
||||
and f"Account {aws_account_number} is not subscribed to AWS Security Hub"
|
||||
in error_message
|
||||
):
|
||||
logger.warning(
|
||||
f"{client_error.__class__.__name__} -- [{client_error.__traceback__.tb_lineno}]: {client_error}"
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f"{client_error.__class__.__name__} -- [{client_error.__traceback__.tb_lineno}]: {client_error}"
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]: {error}"
|
||||
)
|
||||
|
||||
finally:
|
||||
return prowler_integration_enabled
|
||||
|
||||
|
||||
def batch_send_to_security_hub(
|
||||
security_hub_findings_per_region: dict,
|
||||
session: session.Session,
|
||||
) -> int:
|
||||
"""
|
||||
send_to_security_hub sends findings to Security Hub and returns the number of findings that were successfully sent.
|
||||
"""
|
||||
|
||||
success_count = 0
|
||||
try:
|
||||
# Iterate findings by region
|
||||
for region, findings in security_hub_findings_per_region.items():
|
||||
# Send findings to Security Hub
|
||||
logger.info(f"Sending findings to Security Hub in the region {region}")
|
||||
|
||||
security_hub_client = session.client("securityhub", region_name=region)
|
||||
|
||||
success_count = __send_findings_to_security_hub__(
|
||||
findings, region, security_hub_client
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
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(
|
||||
security_hub_findings_per_region: dict, provider
|
||||
) -> 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.")
|
||||
success_count = 0
|
||||
for region in security_hub_findings_per_region.keys():
|
||||
try:
|
||||
current_findings = security_hub_findings_per_region[region]
|
||||
# Get current findings IDs
|
||||
current_findings_ids = []
|
||||
for finding in current_findings:
|
||||
current_findings_ids.append(finding["Id"])
|
||||
# Get findings of that region
|
||||
security_hub_client = provider.session.current_session.client(
|
||||
"securityhub", region_name=region
|
||||
)
|
||||
findings_filter = {
|
||||
"ProductName": [{"Value": "Prowler", "Comparison": "EQUALS"}],
|
||||
"RecordState": [{"Value": "ACTIVE", "Comparison": "EQUALS"}],
|
||||
"AwsAccountId": [
|
||||
{"Value": provider.identity.account, "Comparison": "EQUALS"}
|
||||
],
|
||||
"Region": [{"Value": region, "Comparison": "EQUALS"}],
|
||||
}
|
||||
get_findings_paginator = security_hub_client.get_paginator("get_findings")
|
||||
findings_to_archive = []
|
||||
for page in get_findings_paginator.paginate(Filters=findings_filter):
|
||||
# Archive findings that have not appear in this execution
|
||||
for finding in page["Findings"]:
|
||||
if finding["Id"] not in current_findings_ids:
|
||||
finding["RecordState"] = "ARCHIVED"
|
||||
finding["UpdatedAt"] = timestamp_utc.strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ"
|
||||
)
|
||||
|
||||
findings_to_archive.append(finding)
|
||||
logger.info(f"Archiving {len(findings_to_archive)} findings.")
|
||||
|
||||
# Send archive findings to SHub
|
||||
success_count += __send_findings_to_security_hub__(
|
||||
findings_to_archive, region, security_hub_client
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
f"{error.__class__.__name__} -- [{error.__traceback__.tb_lineno}]:{error} in region {region}"
|
||||
)
|
||||
return success_count
|
||||
|
||||
|
||||
def __send_findings_to_security_hub__(
|
||||
findings: list[dict], region: str, security_hub_client
|
||||
):
|
||||
"""Private function send_findings_to_security_hub chunks the findings in groups of 100 findings and send them to AWS Security Hub. It returns the number of sent findings."""
|
||||
success_count = 0
|
||||
try:
|
||||
list_chunked = [
|
||||
findings[i : i + SECURITY_HUB_MAX_BATCH]
|
||||
for i in range(0, len(findings), SECURITY_HUB_MAX_BATCH)
|
||||
]
|
||||
|
||||
for findings in list_chunked:
|
||||
batch_import = security_hub_client.batch_import_findings(Findings=findings)
|
||||
if batch_import["FailedCount"] > 0:
|
||||
failed_import = batch_import["FailedFindings"][0]
|
||||
logger.error(
|
||||
f"Failed to send 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.__traceback__.tb_lineno}]:{error} in region {region}"
|
||||
)
|
||||
finally:
|
||||
return success_count
|
||||
@@ -7,6 +7,7 @@ from prowler.providers.common.provider import Provider
|
||||
|
||||
|
||||
# TODO: include this for all the providers
|
||||
# Rename to AuditMetadata or ScanMetadata
|
||||
class Audit_Metadata(BaseModel):
|
||||
services_scanned: int
|
||||
# We can't use a set in the expected
|
||||
|
||||
File renamed without changes.
+90
-114
@@ -1,21 +1,16 @@
|
||||
from argparse import Namespace
|
||||
from logging import ERROR, WARNING
|
||||
from os import path
|
||||
|
||||
import botocore
|
||||
from boto3 import session
|
||||
from botocore.client import ClientError
|
||||
from mock import MagicMock, patch
|
||||
from mock import patch
|
||||
|
||||
from prowler.config.config import prowler_version, timestamp_utc
|
||||
from prowler.lib.check.models import Check_Report, load_check_metadata
|
||||
from prowler.providers.aws.lib.security_hub.security_hub import (
|
||||
batch_send_to_security_hub,
|
||||
prepare_security_hub_findings,
|
||||
verify_security_hub_integration_enabled_per_region,
|
||||
)
|
||||
from prowler.lib.outputs.security_hub.security_hub import SecurityHub
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_COMMERCIAL_PARTITION,
|
||||
AWS_REGION_EU_WEST_1,
|
||||
AWS_REGION_EU_WEST_2,
|
||||
set_mocked_aws_provider,
|
||||
@@ -108,37 +103,25 @@ class Test_SecurityHub:
|
||||
|
||||
return finding
|
||||
|
||||
def set_mocked_output_options(
|
||||
self, status: list[str] = [], send_sh_only_fails: bool = False
|
||||
):
|
||||
output_options = MagicMock
|
||||
output_options.bulk_checks_metadata = {}
|
||||
output_options.status = status
|
||||
output_options.send_sh_only_fails = send_sh_only_fails
|
||||
|
||||
return output_options
|
||||
|
||||
def set_mocked_session(self, region):
|
||||
# Create mock session
|
||||
return session.Session(
|
||||
region_name=region,
|
||||
)
|
||||
|
||||
@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call)
|
||||
def test_verify_security_hub_integration_enabled_per_region(self):
|
||||
session = self.set_mocked_session(AWS_REGION_EU_WEST_1)
|
||||
assert verify_security_hub_integration_enabled_per_region(
|
||||
AWS_COMMERCIAL_PARTITION, AWS_REGION_EU_WEST_1, session, AWS_ACCOUNT_NUMBER
|
||||
aws_provider = set_mocked_aws_provider()
|
||||
security_hub = SecurityHub(aws_provider)
|
||||
|
||||
assert security_hub.verify_security_hub_integration_enabled_per_region(
|
||||
AWS_REGION_EU_WEST_1
|
||||
)
|
||||
|
||||
def test_verify_security_hub_integration_enabled_per_region_security_hub_disabled(
|
||||
self, caplog
|
||||
):
|
||||
aws_provider = set_mocked_aws_provider()
|
||||
security_hub = SecurityHub(aws_provider)
|
||||
|
||||
caplog.set_level(WARNING)
|
||||
session = self.set_mocked_session(AWS_REGION_EU_WEST_1)
|
||||
|
||||
with patch(
|
||||
"prowler.providers.aws.lib.security_hub.security_hub.session.Session.client",
|
||||
"prowler.lib.outputs.security_hub.security_hub.Session.client",
|
||||
) as mock_security_hub:
|
||||
error_message = f"Account {AWS_ACCOUNT_NUMBER} is not subscribed to AWS Security Hub in region {AWS_REGION_EU_WEST_1}"
|
||||
error_code = "InvalidAccessException"
|
||||
@@ -151,37 +134,33 @@ class Test_SecurityHub:
|
||||
operation_name = "DescribeHub"
|
||||
mock_security_hub.side_effect = ClientError(error_response, operation_name)
|
||||
|
||||
assert not verify_security_hub_integration_enabled_per_region(
|
||||
AWS_COMMERCIAL_PARTITION,
|
||||
assert not security_hub.verify_security_hub_integration_enabled_per_region(
|
||||
AWS_REGION_EU_WEST_1,
|
||||
session,
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
)
|
||||
assert caplog.record_tuples == [
|
||||
(
|
||||
"root",
|
||||
WARNING,
|
||||
f"ClientError -- [70]: An error occurred ({error_code}) when calling the {operation_name} operation: {error_message}",
|
||||
f"ClientError -- [86]: An error occurred ({error_code}) when calling the {operation_name} operation: {error_message}",
|
||||
)
|
||||
]
|
||||
|
||||
def test_verify_security_hub_integration_enabled_per_region_prowler_not_subscribed(
|
||||
self, caplog
|
||||
):
|
||||
aws_provider = set_mocked_aws_provider()
|
||||
security_hub = SecurityHub(aws_provider)
|
||||
|
||||
caplog.set_level(WARNING)
|
||||
session = self.set_mocked_session(AWS_REGION_EU_WEST_1)
|
||||
|
||||
with patch(
|
||||
"prowler.providers.aws.lib.security_hub.security_hub.session.Session.client",
|
||||
"prowler.lib.outputs.security_hub.security_hub.Session.client",
|
||||
) as mock_security_hub:
|
||||
mock_security_hub.describe_hub.return_value = None
|
||||
mock_security_hub.list_enabled_products_for_import.return_value = []
|
||||
|
||||
assert not verify_security_hub_integration_enabled_per_region(
|
||||
AWS_COMMERCIAL_PARTITION,
|
||||
assert not security_hub.verify_security_hub_integration_enabled_per_region(
|
||||
AWS_REGION_EU_WEST_1,
|
||||
session,
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
)
|
||||
assert caplog.record_tuples == [
|
||||
(
|
||||
@@ -194,11 +173,13 @@ class Test_SecurityHub:
|
||||
def test_verify_security_hub_integration_enabled_per_region_another_ClientError(
|
||||
self, caplog
|
||||
):
|
||||
aws_provider = set_mocked_aws_provider()
|
||||
security_hub = SecurityHub(aws_provider)
|
||||
|
||||
caplog.set_level(WARNING)
|
||||
session = self.set_mocked_session(AWS_REGION_EU_WEST_1)
|
||||
|
||||
with patch(
|
||||
"prowler.providers.aws.lib.security_hub.security_hub.session.Session.client",
|
||||
"prowler.lib.outputs.security_hub.security_hub.Session.client",
|
||||
) as mock_security_hub:
|
||||
error_message = f"Another exception in region {AWS_REGION_EU_WEST_1}"
|
||||
error_code = "AnotherException"
|
||||
@@ -211,58 +192,52 @@ class Test_SecurityHub:
|
||||
operation_name = "DescribeHub"
|
||||
mock_security_hub.side_effect = ClientError(error_response, operation_name)
|
||||
|
||||
assert not verify_security_hub_integration_enabled_per_region(
|
||||
AWS_COMMERCIAL_PARTITION,
|
||||
assert not security_hub.verify_security_hub_integration_enabled_per_region(
|
||||
AWS_REGION_EU_WEST_1,
|
||||
session,
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
)
|
||||
assert caplog.record_tuples == [
|
||||
(
|
||||
"root",
|
||||
ERROR,
|
||||
f"ClientError -- [70]: An error occurred ({error_code}) when calling the {operation_name} operation: {error_message}",
|
||||
f"ClientError -- [86]: An error occurred ({error_code}) when calling the {operation_name} operation: {error_message}",
|
||||
)
|
||||
]
|
||||
|
||||
def test_verify_security_hub_integration_enabled_per_region_another_Exception(
|
||||
self, caplog
|
||||
):
|
||||
aws_provider = set_mocked_aws_provider()
|
||||
security_hub = SecurityHub(aws_provider)
|
||||
|
||||
caplog.set_level(WARNING)
|
||||
session = self.set_mocked_session(AWS_REGION_EU_WEST_1)
|
||||
|
||||
with patch(
|
||||
"prowler.providers.aws.lib.security_hub.security_hub.session.Session.client",
|
||||
"prowler.lib.outputs.security_hub.security_hub.Session.client",
|
||||
) as mock_security_hub:
|
||||
error_message = f"Another exception in region {AWS_REGION_EU_WEST_1}"
|
||||
mock_security_hub.side_effect = Exception(error_message)
|
||||
|
||||
assert not verify_security_hub_integration_enabled_per_region(
|
||||
AWS_COMMERCIAL_PARTITION,
|
||||
assert not security_hub.verify_security_hub_integration_enabled_per_region(
|
||||
AWS_REGION_EU_WEST_1,
|
||||
session,
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
)
|
||||
assert caplog.record_tuples == [
|
||||
(
|
||||
"root",
|
||||
ERROR,
|
||||
f"Exception -- [70]: {error_message}",
|
||||
f"Exception -- [86]: {error_message}",
|
||||
)
|
||||
]
|
||||
|
||||
def test_prepare_security_hub_findings_enabled_region_all_statuses(self):
|
||||
enabled_regions = [AWS_REGION_EU_WEST_1]
|
||||
output_options = self.set_mocked_output_options()
|
||||
findings = [self.generate_finding("PASS", AWS_REGION_EU_WEST_1)]
|
||||
aws_provider = set_mocked_aws_provider(
|
||||
audited_regions=[AWS_REGION_EU_WEST_1, AWS_REGION_EU_WEST_2]
|
||||
)
|
||||
security_hub = SecurityHub(aws_provider)
|
||||
|
||||
assert prepare_security_hub_findings(
|
||||
assert security_hub.prepare_security_hub_findings(
|
||||
findings,
|
||||
aws_provider,
|
||||
output_options,
|
||||
enabled_regions,
|
||||
) == {
|
||||
AWS_REGION_EU_WEST_1: [get_security_hub_finding("PASSED")],
|
||||
@@ -270,104 +245,109 @@ class Test_SecurityHub:
|
||||
|
||||
def test_prepare_security_hub_findings_all_statuses_MANUAL_finding(self):
|
||||
enabled_regions = [AWS_REGION_EU_WEST_1]
|
||||
output_options = self.set_mocked_output_options()
|
||||
findings = [self.generate_finding("MANUAL", AWS_REGION_EU_WEST_1)]
|
||||
aws_provider = set_mocked_aws_provider(
|
||||
audited_regions=[AWS_REGION_EU_WEST_1, AWS_REGION_EU_WEST_2]
|
||||
)
|
||||
security_hub = SecurityHub(aws_provider)
|
||||
|
||||
assert prepare_security_hub_findings(
|
||||
assert security_hub.prepare_security_hub_findings(
|
||||
findings,
|
||||
aws_provider,
|
||||
output_options,
|
||||
enabled_regions,
|
||||
) == {AWS_REGION_EU_WEST_1: []}
|
||||
|
||||
def test_prepare_security_hub_findings_disabled_region(self):
|
||||
enabled_regions = [AWS_REGION_EU_WEST_1]
|
||||
output_options = self.set_mocked_output_options()
|
||||
findings = [self.generate_finding("PASS", AWS_REGION_EU_WEST_2)]
|
||||
aws_provider = set_mocked_aws_provider(
|
||||
audited_regions=[AWS_REGION_EU_WEST_1, AWS_REGION_EU_WEST_2]
|
||||
)
|
||||
security_hub = SecurityHub(aws_provider)
|
||||
|
||||
assert prepare_security_hub_findings(
|
||||
assert security_hub.prepare_security_hub_findings(
|
||||
findings,
|
||||
aws_provider,
|
||||
output_options,
|
||||
enabled_regions,
|
||||
) == {AWS_REGION_EU_WEST_1: []}
|
||||
|
||||
def test_prepare_security_hub_findings_PASS_and_FAIL_statuses(self):
|
||||
enabled_regions = [AWS_REGION_EU_WEST_1]
|
||||
output_options = self.set_mocked_output_options(status=["FAIL"])
|
||||
findings = [self.generate_finding("PASS", AWS_REGION_EU_WEST_1)]
|
||||
|
||||
args = Namespace()
|
||||
args.status = ["FAIL"]
|
||||
aws_provider = set_mocked_aws_provider(
|
||||
audited_regions=[AWS_REGION_EU_WEST_1, AWS_REGION_EU_WEST_2]
|
||||
audited_regions=[AWS_REGION_EU_WEST_1, AWS_REGION_EU_WEST_2], arguments=args
|
||||
)
|
||||
|
||||
assert prepare_security_hub_findings(
|
||||
security_hub = SecurityHub(aws_provider)
|
||||
|
||||
assert security_hub.prepare_security_hub_findings(
|
||||
findings,
|
||||
aws_provider,
|
||||
output_options,
|
||||
enabled_regions,
|
||||
) == {AWS_REGION_EU_WEST_1: []}
|
||||
|
||||
def test_prepare_security_hub_findings_FAIL_and_FAIL_statuses(self):
|
||||
enabled_regions = [AWS_REGION_EU_WEST_1]
|
||||
output_options = self.set_mocked_output_options(status=["FAIL"])
|
||||
findings = [self.generate_finding("FAIL", AWS_REGION_EU_WEST_1)]
|
||||
|
||||
args = Namespace()
|
||||
args.status = ["FAIL"]
|
||||
aws_provider = set_mocked_aws_provider(
|
||||
audited_regions=[AWS_REGION_EU_WEST_1, AWS_REGION_EU_WEST_2]
|
||||
audited_regions=[AWS_REGION_EU_WEST_1, AWS_REGION_EU_WEST_2], arguments=args
|
||||
)
|
||||
|
||||
assert prepare_security_hub_findings(
|
||||
security_hub = SecurityHub(aws_provider)
|
||||
|
||||
assert security_hub.prepare_security_hub_findings(
|
||||
findings,
|
||||
aws_provider,
|
||||
output_options,
|
||||
enabled_regions,
|
||||
) == {AWS_REGION_EU_WEST_1: [get_security_hub_finding("FAILED")]}
|
||||
|
||||
def test_prepare_security_hub_findings_send_sh_only_fails_PASS(self):
|
||||
enabled_regions = [AWS_REGION_EU_WEST_1]
|
||||
output_options = self.set_mocked_output_options(send_sh_only_fails=True)
|
||||
findings = [self.generate_finding("PASS", AWS_REGION_EU_WEST_1)]
|
||||
aws_provider = set_mocked_aws_provider(
|
||||
audited_regions=[AWS_REGION_EU_WEST_1, AWS_REGION_EU_WEST_2]
|
||||
)
|
||||
|
||||
assert prepare_security_hub_findings(
|
||||
args = Namespace()
|
||||
args.send_sh_only_fails = True
|
||||
|
||||
aws_provider = set_mocked_aws_provider(
|
||||
audited_regions=[AWS_REGION_EU_WEST_1, AWS_REGION_EU_WEST_2], arguments=args
|
||||
)
|
||||
security_hub = SecurityHub(aws_provider)
|
||||
|
||||
assert security_hub.prepare_security_hub_findings(
|
||||
findings,
|
||||
aws_provider,
|
||||
output_options,
|
||||
enabled_regions,
|
||||
) == {AWS_REGION_EU_WEST_1: []}
|
||||
|
||||
def test_prepare_security_hub_findings_send_sh_only_fails_FAIL(self):
|
||||
enabled_regions = [AWS_REGION_EU_WEST_1]
|
||||
output_options = self.set_mocked_output_options(send_sh_only_fails=True)
|
||||
findings = [self.generate_finding("FAIL", AWS_REGION_EU_WEST_1)]
|
||||
aws_provider = set_mocked_aws_provider(
|
||||
audited_regions=[AWS_REGION_EU_WEST_1, AWS_REGION_EU_WEST_2]
|
||||
)
|
||||
|
||||
assert prepare_security_hub_findings(
|
||||
args = Namespace()
|
||||
args.send_sh_only_fails = True
|
||||
|
||||
aws_provider = set_mocked_aws_provider(
|
||||
audited_regions=[AWS_REGION_EU_WEST_1, AWS_REGION_EU_WEST_2], arguments=args
|
||||
)
|
||||
security_hub = SecurityHub(aws_provider)
|
||||
|
||||
assert security_hub.prepare_security_hub_findings(
|
||||
findings,
|
||||
aws_provider,
|
||||
output_options,
|
||||
enabled_regions,
|
||||
) == {AWS_REGION_EU_WEST_1: [get_security_hub_finding("FAILED")]}
|
||||
|
||||
def test_prepare_security_hub_findings_no_audited_regions(self):
|
||||
enabled_regions = [AWS_REGION_EU_WEST_1]
|
||||
output_options = self.set_mocked_output_options()
|
||||
findings = [self.generate_finding("PASS", AWS_REGION_EU_WEST_1)]
|
||||
aws_provider = set_mocked_aws_provider()
|
||||
|
||||
assert prepare_security_hub_findings(
|
||||
aws_provider = set_mocked_aws_provider(
|
||||
audited_regions=[AWS_REGION_EU_WEST_1, AWS_REGION_EU_WEST_2]
|
||||
)
|
||||
security_hub = SecurityHub(aws_provider)
|
||||
|
||||
assert security_hub.prepare_security_hub_findings(
|
||||
findings,
|
||||
aws_provider,
|
||||
output_options,
|
||||
enabled_regions,
|
||||
) == {
|
||||
AWS_REGION_EU_WEST_1: [get_security_hub_finding("PASSED")],
|
||||
@@ -375,20 +355,19 @@ class Test_SecurityHub:
|
||||
|
||||
def test_prepare_security_hub_findings_muted_fail_with_send_sh_only_fails(self):
|
||||
enabled_regions = [AWS_REGION_EU_WEST_1]
|
||||
output_options = self.set_mocked_output_options(
|
||||
send_sh_only_fails=True,
|
||||
)
|
||||
findings = [
|
||||
self.generate_finding(
|
||||
status="FAIL", region=AWS_REGION_EU_WEST_1, muted=True
|
||||
)
|
||||
]
|
||||
aws_provider = set_mocked_aws_provider()
|
||||
args = Namespace()
|
||||
args.send_sh_only_fails = True
|
||||
|
||||
assert prepare_security_hub_findings(
|
||||
aws_provider = set_mocked_aws_provider(arguments=args)
|
||||
security_hub = SecurityHub(aws_provider)
|
||||
|
||||
assert security_hub.prepare_security_hub_findings(
|
||||
findings,
|
||||
aws_provider,
|
||||
output_options,
|
||||
enabled_regions,
|
||||
) == {
|
||||
AWS_REGION_EU_WEST_1: [],
|
||||
@@ -396,18 +375,18 @@ class Test_SecurityHub:
|
||||
|
||||
def test_prepare_security_hub_findings_muted_fail_with_status_FAIL(self):
|
||||
enabled_regions = [AWS_REGION_EU_WEST_1]
|
||||
output_options = self.set_mocked_output_options(status=["FAIL"])
|
||||
findings = [
|
||||
self.generate_finding(
|
||||
status="FAIL", region=AWS_REGION_EU_WEST_1, muted=True
|
||||
)
|
||||
]
|
||||
aws_provider = set_mocked_aws_provider()
|
||||
args = Namespace()
|
||||
args.status = ["FAIL"]
|
||||
aws_provider = set_mocked_aws_provider(arguments=args)
|
||||
security_hub = SecurityHub(aws_provider)
|
||||
|
||||
assert prepare_security_hub_findings(
|
||||
assert security_hub.prepare_security_hub_findings(
|
||||
findings,
|
||||
aws_provider,
|
||||
output_options,
|
||||
enabled_regions,
|
||||
) == {
|
||||
AWS_REGION_EU_WEST_1: [],
|
||||
@@ -416,24 +395,21 @@ class Test_SecurityHub:
|
||||
@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call)
|
||||
def test_batch_send_to_security_hub_one_finding(self):
|
||||
enabled_regions = [AWS_REGION_EU_WEST_1]
|
||||
output_options = self.set_mocked_output_options()
|
||||
findings = [self.generate_finding("PASS", AWS_REGION_EU_WEST_1)]
|
||||
|
||||
aws_provider = set_mocked_aws_provider(
|
||||
audited_regions=[AWS_REGION_EU_WEST_1, AWS_REGION_EU_WEST_2]
|
||||
)
|
||||
session = self.set_mocked_session(AWS_REGION_EU_WEST_1)
|
||||
security_hub = SecurityHub(aws_provider)
|
||||
|
||||
security_hub_findings = prepare_security_hub_findings(
|
||||
security_hub_findings = security_hub.prepare_security_hub_findings(
|
||||
findings,
|
||||
aws_provider,
|
||||
output_options,
|
||||
enabled_regions,
|
||||
)
|
||||
|
||||
assert (
|
||||
batch_send_to_security_hub(
|
||||
security_hub.batch_send_to_security_hub(
|
||||
security_hub_findings,
|
||||
session,
|
||||
)
|
||||
== 1
|
||||
)
|
||||
@@ -1,13 +1,7 @@
|
||||
import sys
|
||||
from unittest import mock
|
||||
|
||||
from prowler.config.config import aws_logo, azure_logo, gcp_logo
|
||||
from prowler.lib.outputs.slack import (
|
||||
create_message_blocks,
|
||||
create_message_identity,
|
||||
create_title,
|
||||
send_slack_message,
|
||||
)
|
||||
from prowler.lib.outputs.slack.slack import Slack
|
||||
from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, set_mocked_aws_provider
|
||||
from tests.providers.azure.azure_fixtures import (
|
||||
AZURE_SUBSCRIPTION_ID,
|
||||
@@ -16,28 +10,25 @@ from tests.providers.azure.azure_fixtures import (
|
||||
)
|
||||
from tests.providers.gcp.gcp_fixtures import set_mocked_gcp_provider
|
||||
|
||||
|
||||
def mock_create_message_blocks(*_):
|
||||
return [{}]
|
||||
|
||||
|
||||
def mock_create_message_identity(*_):
|
||||
return "", ""
|
||||
SLACK_CHANNEL = "test-channel"
|
||||
SLACK_TOKEN = "test-token"
|
||||
|
||||
|
||||
class TestSlackIntegration:
|
||||
def test_create_message_identity_aws(self):
|
||||
aws_provider = set_mocked_aws_provider()
|
||||
slack = Slack(SLACK_TOKEN, SLACK_CHANNEL, aws_provider)
|
||||
|
||||
assert create_message_identity(aws_provider) == (
|
||||
assert slack.__create_message_identity__(aws_provider) == (
|
||||
f"AWS Account *{aws_provider.identity.account}*",
|
||||
aws_logo,
|
||||
)
|
||||
|
||||
def test_create_message_identity_azure(self):
|
||||
azure_provider = set_mocked_azure_provider()
|
||||
slack = Slack(SLACK_TOKEN, SLACK_CHANNEL, azure_provider)
|
||||
|
||||
assert create_message_identity(azure_provider) == (
|
||||
assert slack.__create_message_identity__(azure_provider) == (
|
||||
f"Azure Subscriptions:\n- *{AZURE_SUBSCRIPTION_ID}: {AZURE_SUBSCRIPTION_NAME}*\n",
|
||||
azure_logo,
|
||||
)
|
||||
@@ -46,27 +37,50 @@ class TestSlackIntegration:
|
||||
gcp_provider = set_mocked_gcp_provider(
|
||||
project_ids=["test-project1", "test-project2"],
|
||||
)
|
||||
slack = Slack(SLACK_TOKEN, SLACK_CHANNEL, gcp_provider)
|
||||
|
||||
assert create_message_identity(gcp_provider) == (
|
||||
assert slack.__create_message_identity__(gcp_provider) == (
|
||||
f"GCP Projects *{', '.join(gcp_provider.project_ids)}*",
|
||||
gcp_logo,
|
||||
)
|
||||
|
||||
def test_create_message_blocks(self):
|
||||
aws_identity = f"AWS Account *{AWS_ACCOUNT_NUMBER}*"
|
||||
azure_identity = "Azure Subscriptions:\n- *subscription 1: qwerty*\n- *subscription 2: asdfg*\n"
|
||||
gcp_identity = "GCP Project *gcp-project*"
|
||||
def test_create_title(self):
|
||||
aws_provider = set_mocked_aws_provider()
|
||||
slack = Slack(SLACK_TOKEN, SLACK_CHANNEL, aws_provider)
|
||||
|
||||
stats = {}
|
||||
stats["total_pass"] = 12
|
||||
stats["total_fail"] = 10
|
||||
stats["resources_count"] = 20
|
||||
stats["findings_count"] = 22
|
||||
assert create_message_blocks(aws_identity, aws_logo, stats) == [
|
||||
|
||||
identity = slack.__create_message_identity__(aws_provider) == (
|
||||
f"AWS Account *{aws_provider.identity.account}*",
|
||||
aws_logo,
|
||||
)
|
||||
assert (
|
||||
slack.__create_title__(identity, stats)
|
||||
== f"Hey there 👋 \n I'm *Prowler*, _the handy multi-cloud security tool_ :cloud::key:\n\n I have just finished the security assessment on your {identity} with a total of *{stats['findings_count']}* findings."
|
||||
)
|
||||
|
||||
def test_create_message_blocks_aws(self):
|
||||
aws_provider = set_mocked_aws_provider()
|
||||
slack = Slack(SLACK_TOKEN, SLACK_CHANNEL, aws_provider)
|
||||
args = "--slack"
|
||||
stats = {}
|
||||
stats["total_pass"] = 12
|
||||
stats["total_fail"] = 10
|
||||
stats["resources_count"] = 20
|
||||
stats["findings_count"] = 22
|
||||
|
||||
aws_identity = f"AWS Account *{AWS_ACCOUNT_NUMBER}*"
|
||||
|
||||
assert slack.__create_message_blocks__(aws_identity, aws_logo, stats, args) == [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": create_title(aws_identity, stats),
|
||||
"text": slack.__create_title__(aws_identity, stats),
|
||||
},
|
||||
"accessory": {
|
||||
"type": "image",
|
||||
@@ -102,7 +116,7 @@ class TestSlackIntegration:
|
||||
"elements": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": f"Used parameters: `prowler {' '.join(sys.argv[1:])} `",
|
||||
"text": f"Used parameters: `prowler {args}`",
|
||||
}
|
||||
],
|
||||
},
|
||||
@@ -141,12 +155,27 @@ class TestSlackIntegration:
|
||||
},
|
||||
},
|
||||
]
|
||||
assert create_message_blocks(azure_identity, azure_logo, stats) == [
|
||||
|
||||
def test_create_message_blocks_azure(self):
|
||||
aws_provider = set_mocked_aws_provider()
|
||||
slack = Slack(SLACK_TOKEN, SLACK_CHANNEL, aws_provider)
|
||||
args = "--slack"
|
||||
stats = {}
|
||||
stats["total_pass"] = 12
|
||||
stats["total_fail"] = 10
|
||||
stats["resources_count"] = 20
|
||||
stats["findings_count"] = 22
|
||||
|
||||
azure_identity = "Azure Subscriptions:\n- *subscription 1: qwerty*\n- *subscription 2: asdfg*\n"
|
||||
|
||||
assert slack.__create_message_blocks__(
|
||||
azure_identity, azure_logo, stats, args
|
||||
) == [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": create_title(azure_identity, stats),
|
||||
"text": slack.__create_title__(azure_identity, stats),
|
||||
},
|
||||
"accessory": {
|
||||
"type": "image",
|
||||
@@ -182,7 +211,7 @@ class TestSlackIntegration:
|
||||
"elements": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": f"Used parameters: `prowler {' '.join(sys.argv[1:])} `",
|
||||
"text": f"Used parameters: `prowler {args}`",
|
||||
}
|
||||
],
|
||||
},
|
||||
@@ -221,12 +250,25 @@ class TestSlackIntegration:
|
||||
},
|
||||
},
|
||||
]
|
||||
assert create_message_blocks(gcp_identity, gcp_logo, stats) == [
|
||||
|
||||
def test_create_message_blocks_gcp(self):
|
||||
aws_provider = set_mocked_aws_provider()
|
||||
slack = Slack(SLACK_TOKEN, SLACK_CHANNEL, aws_provider)
|
||||
args = "--slack"
|
||||
stats = {}
|
||||
stats["total_pass"] = 12
|
||||
stats["total_fail"] = 10
|
||||
stats["resources_count"] = 20
|
||||
stats["findings_count"] = 22
|
||||
|
||||
gcp_identity = "GCP Project *gcp-project*"
|
||||
|
||||
assert slack.__create_message_blocks__(gcp_identity, gcp_logo, stats, args) == [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": create_title(gcp_identity, stats),
|
||||
"text": slack.__create_title__(gcp_identity, stats),
|
||||
},
|
||||
"accessory": {
|
||||
"type": "image",
|
||||
@@ -262,7 +304,7 @@ class TestSlackIntegration:
|
||||
"elements": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": f"Used parameters: `prowler {' '.join(sys.argv[1:])} `",
|
||||
"text": f"Used parameters: `prowler {args}`",
|
||||
}
|
||||
],
|
||||
},
|
||||
@@ -324,14 +366,13 @@ class TestSlackIntegration:
|
||||
mocked_web_client.chat_postMessage = mock.Mock(
|
||||
return_value=mocked_slack_response
|
||||
)
|
||||
|
||||
with mock.patch(
|
||||
"prowler.lib.outputs.slack.create_message_blocks",
|
||||
new=mock_create_message_blocks,
|
||||
), mock.patch(
|
||||
"prowler.lib.outputs.slack.create_message_identity",
|
||||
new=mock_create_message_identity,
|
||||
), mock.patch(
|
||||
"prowler.lib.outputs.slack.WebClient", new=mocked_web_client
|
||||
"prowler.lib.outputs.slack.slack.WebClient", new=mocked_web_client
|
||||
):
|
||||
response = send_slack_message("test-token", "test-channel", {}, "provider")
|
||||
aws_provider = set_mocked_aws_provider()
|
||||
slack = Slack(SLACK_TOKEN, SLACK_CHANNEL, aws_provider)
|
||||
stats = {}
|
||||
args = "--slack"
|
||||
response = slack.send(stats, args)
|
||||
assert response == mocked_slack_response
|
||||
@@ -153,8 +153,12 @@ def set_mocked_aws_provider(
|
||||
return provider
|
||||
|
||||
|
||||
def set_default_provider_arguments(arguments: Namespace) -> Namespace:
|
||||
def set_default_provider_arguments(input_arguments: Namespace) -> Namespace:
|
||||
|
||||
arguments = Namespace
|
||||
arguments.status = []
|
||||
if hasattr(input_arguments, "status") and input_arguments.status:
|
||||
arguments.status = input_arguments.status
|
||||
arguments.output_formats = []
|
||||
arguments.output_directory = ""
|
||||
arguments.verbose = False
|
||||
@@ -162,7 +166,14 @@ def set_default_provider_arguments(arguments: Namespace) -> Namespace:
|
||||
arguments.unix_timestamp = False
|
||||
arguments.shodan = None
|
||||
arguments.security_hub = False
|
||||
|
||||
arguments.send_sh_only_fails = False
|
||||
if (
|
||||
hasattr(input_arguments, "send_sh_only_fails")
|
||||
and input_arguments.send_sh_only_fails
|
||||
):
|
||||
arguments.send_sh_only_fails = input_arguments.send_sh_only_fails
|
||||
|
||||
arguments.config_file = default_config_file_path
|
||||
arguments.fixer_config = default_fixer_config_file_path
|
||||
|
||||
|
||||
Reference in new issue
Block a user