chore(CLI): run a Prowler scan (#4182)

Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
This commit is contained in:
Pedro MartínandPepe Fagoaga authored and GitHub committed 2024-06-05 09:01:42 +02:00
1 parent 31a770848a
commit 63e5318a2c
15 files changed
+868 -611

No files matched your search

+62
View File
@@ -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
View File
@@ -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
View File
@@ -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(
@@ -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
-156
View File
@@ -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.
+206
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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
@@ -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
+12 -1
View File
@@ -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