diff --git a/prowler/__main__.py b/prowler/__main__.py index 15e0c185..791dd8f4 100644 --- a/prowler/__main__.py +++ b/prowler/__main__.py @@ -58,6 +58,9 @@ def prowler(): severities = args.severity compliance_framework = args.compliance + if not args.no_banner: + print_banner(args) + # Set the audit info based on the selected provider audit_info = set_provider_audit_info(provider, args.__dict__) @@ -72,9 +75,6 @@ def prowler(): # Set Logger configuration set_logging_config(args.log_level, args.log_file, args.only_logs) - if not args.no_banner: - print_banner(args) - if args.list_services: print_services(list_services(provider)) sys.exit() diff --git a/prowler/lib/outputs/file_descriptors.py b/prowler/lib/outputs/file_descriptors.py index 5fda584e..e40b6d0d 100644 --- a/prowler/lib/outputs/file_descriptors.py +++ b/prowler/lib/outputs/file_descriptors.py @@ -100,6 +100,13 @@ def fill_file_descriptors(output_modes, output_directory, output_filename, audit ) file_descriptors.update({output_mode: file_descriptor}) + elif output_mode == "html": + filename = f"{output_directory}/{output_filename}{html_file_suffix}" + file_descriptor = initialize_file_descriptor( + filename, output_mode, audit_info + ) + file_descriptors.update({output_mode: file_descriptor}) + elif isinstance(audit_info, AWS_Audit_Info): if output_mode == "json-asff": filename = f"{output_directory}/{output_filename}{json_asff_file_suffix}" @@ -108,15 +115,6 @@ def fill_file_descriptors(output_modes, output_directory, output_filename, audit ) file_descriptors.update({output_mode: file_descriptor}) - elif output_mode == "html": - filename = ( - f"{output_directory}/{output_filename}{html_file_suffix}" - ) - file_descriptor = initialize_file_descriptor( - filename, output_mode, audit_info - ) - file_descriptors.update({output_mode: file_descriptor}) - elif output_mode == "ens_rd2022_aws": filename = f"{output_directory}/{output_filename}_ens_rd2022_aws{csv_file_suffix}" file_descriptor = initialize_file_descriptor( diff --git a/prowler/lib/outputs/html.py b/prowler/lib/outputs/html.py index 99b74d1d..a25aacf1 100644 --- a/prowler/lib/outputs/html.py +++ b/prowler/lib/outputs/html.py @@ -1,3 +1,4 @@ +import importlib import sys from os import path @@ -8,6 +9,7 @@ from prowler.config.config import ( prowler_version, timestamp, ) +from prowler.lib.check.models import Check_Report_AWS, Check_Report_GCP from prowler.lib.logger import logger from prowler.lib.outputs.models import ( get_check_compliance, @@ -16,18 +18,13 @@ from prowler.lib.outputs.models import ( unroll_tags, ) from prowler.lib.utils.utils import open_file +from prowler.providers.aws.lib.audit_info.models import AWS_Audit_Info +from prowler.providers.azure.lib.audit_info.models import Azure_Audit_Info +from prowler.providers.gcp.lib.audit_info.models import GCP_Audit_Info def add_html_header(file_descriptor, audit_info): try: - if not audit_info.profile: - audit_info.profile = "ENV" - if isinstance(audit_info.audited_regions, list): - audited_regions = " ".join(audit_info.audited_regions) - elif not audit_info.audited_regions: - audited_regions = "All Regions" - else: - audited_regions = audit_info.audited_regions file_descriptor.write( """ @@ -114,51 +111,9 @@ def add_html_header(file_descriptor, audit_info): - -
-
-
- AWS Assessment Summary -
-
""" + + get_assessment_summary(audit_info) + """ - -
  • - AWS-CLI Profile: """ - + audit_info.profile - + """ -
  • -
  • - Audited Regions: """ - + audited_regions - + """ -
  • - -
    - -
    -
    -
    - AWS Credentials -
    - -
    -
    @@ -205,9 +160,10 @@ def add_html_header(file_descriptor, audit_info): """ ) except Exception as error: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" ) + sys.exit(1) def fill_html(file_descriptor, finding, output_options): @@ -225,7 +181,7 @@ def fill_html(file_descriptor, finding, output_options): {finding.status} {finding.check_metadata.Severity} {finding.check_metadata.ServiceName} - {finding.region} + {finding.location if isinstance(finding, Check_Report_GCP) else finding.region if isinstance(finding, Check_Report_AWS) else ""} {finding.check_metadata.CheckID.replace("_", "_")} {finding.check_metadata.CheckTitle} {finding.resource_id.replace("<", "<").replace(">", ">").replace("_", "_")} @@ -377,3 +333,207 @@ def add_html_footer(output_filename, output_directory): f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" ) sys.exit(1) + + +def get_aws_html_assessment_summary(audit_info): + try: + if isinstance(audit_info, AWS_Audit_Info): + if not audit_info.profile: + audit_info.profile = "ENV" + if isinstance(audit_info.audited_regions, list): + audited_regions = " ".join(audit_info.audited_regions) + elif not audit_info.audited_regions: + audited_regions = "All Regions" + else: + audited_regions = audit_info.audited_regions + return ( + """ +
    +
    +
    + AWS Assessment Summary +
    +
      +
    • + AWS Account: """ + + audit_info.audited_account + + """ +
    • +
    • + AWS-CLI Profile: """ + + audit_info.profile + + """ +
    • +
    • + Audited Regions: """ + + audited_regions + + """ +
    • +
    +
    +
    +
    +
    +
    + AWS Credentials +
    +
      +
    • + User Id: """ + + audit_info.audited_user_id + + """ +
    • +
    • + Caller Identity ARN: """ + + audit_info.audited_identity_arn + + """ +
    • +
    +
    +
    + """ + ) + + except Exception as error: + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + sys.exit(1) + + +def get_azure_html_assessment_summary(audit_info): + try: + if isinstance(audit_info, Azure_Audit_Info): + printed_subscriptions = [] + for key, value in audit_info.identity.subscriptions.items(): + intermediate = key + " : " + value + printed_subscriptions.append(intermediate) + return ( + """ +
    +
    +
    + Azure Assessment Summary +
    +
      +
    • + Azure Tenant IDs: """ + + " ".join(audit_info.identity.tenant_ids) + + """ +
    • +
    • + Azure Tenant Domain: """ + + audit_info.identity.domain + + """ +
    • +
    • + Azure Subscriptions: """ + + " ".join(printed_subscriptions) + + """ +
    • +
    +
    +
    +
    +
    +
    + Azure Credentials +
    +
      +
    • + Azure Identity Type: """ + + audit_info.identity.identity_type + + """ +
    • +
    • + Azure Identity ID: """ + + audit_info.identity.identity_id + + """ +
    • +
    +
    +
    + """ + ) + except Exception as error: + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + sys.exit(1) + + +def get_gcp_html_assessment_summary(audit_info): + try: + if isinstance(audit_info, GCP_Audit_Info): + try: + getattr(audit_info.credentials, "_service_account_email") + profile = ( + audit_info.credentials._service_account_email + if audit_info.credentials._service_account_email is not None + else "default" + ) + except AttributeError: + profile = "default" + return ( + """ +
    +
    +
    + GCP Assessment Summary +
    +
      +
    • + GCP Project ID: """ + + audit_info.project_id + + """ +
    • +
    +
    +
    +
    +
    +
    + GCP Credentials +
    +
      +
    • + GCP Account: """ + + profile + + """ +
    • +
    +
    +
    + """ + ) + except Exception as error: + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + sys.exit(1) + + +def get_assessment_summary(audit_info): + """ + get_assessment_summary gets the HTML assessment summary for the provider + """ + try: + # This is based in the Provider_Audit_Info class + # It is not pretty but useful + # AWS_Audit_Info --> aws + # GCP_Audit_Info --> gcp + # Azure_Audit_Info --> azure + provider = audit_info.__class__.__name__.split("_")[0].lower() + + # Dynamically get the Provider quick inventory handler + provider_html_assessment_summary_function = ( + f"get_{provider}_html_assessment_summary" + ) + return getattr( + importlib.import_module(__name__), provider_html_assessment_summary_function + )(audit_info) + except Exception as error: + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + sys.exit(1) diff --git a/prowler/lib/outputs/outputs.py b/prowler/lib/outputs/outputs.py index 3a1ffb59..8f4ffd7a 100644 --- a/prowler/lib/outputs/outputs.py +++ b/prowler/lib/outputs/outputs.py @@ -104,12 +104,6 @@ def report(check_findings, output_options, audit_info): file_descriptors, ) - if "html" in file_descriptors: - fill_html( - file_descriptors["html"], finding, output_options - ) - file_descriptors["html"].write("") - if "json-asff" in file_descriptors: finding_output = Check_Output_JSON_ASFF() fill_json_asff( @@ -137,6 +131,10 @@ def report(check_findings, output_options, audit_info): ) # Common outputs + if "html" in file_descriptors: + fill_html(file_descriptors["html"], finding, output_options) + file_descriptors["html"].write("") + if "csv" in file_descriptors: csv_writer, finding_output = generate_provider_output_csv( finding.check_metadata.Provider, diff --git a/prowler/providers/common/audit_info.py b/prowler/providers/common/audit_info.py index 10e6d124..cd4f8b82 100644 --- a/prowler/providers/common/audit_info.py +++ b/prowler/providers/common/audit_info.py @@ -96,7 +96,7 @@ This report is being generated using the identity below: Azure Tenant IDs: {Fore.YELLOW}[{" ".join(audit_info.identity.tenant_ids)}]{Style.RESET_ALL} Azure Tenant Domain: {Fore.YELLOW}[{audit_info.identity.domain}]{Style.RESET_ALL} Azure Subscriptions: {Fore.YELLOW}{printed_subscriptions}{Style.RESET_ALL} -Azure Identity type: {Fore.YELLOW}[{audit_info.identity.identity_type}]{Style.RESET_ALL} Azure Identity ID: {Fore.YELLOW}[{audit_info.identity.identity_id}]{Style.RESET_ALL} +Azure Identity Type: {Fore.YELLOW}[{audit_info.identity.identity_type}]{Style.RESET_ALL} Azure Identity ID: {Fore.YELLOW}[{audit_info.identity.identity_id}]{Style.RESET_ALL} """ print(report) diff --git a/prowler/providers/common/outputs.py b/prowler/providers/common/outputs.py index 8ac12834..adf4bf6f 100644 --- a/prowler/providers/common/outputs.py +++ b/prowler/providers/common/outputs.py @@ -75,10 +75,6 @@ class Azure_Output_Options(Provider_Output_Options): else: self.output_filename = arguments.output_filename - # Remove HTML Output since it is not supported yet - if "html" in arguments.output_modes: - arguments.output_modes.remove("html") - class Gcp_Output_Options(Provider_Output_Options): def __init__(self, arguments, audit_info, allowlist_file, bulk_checks_metadata): @@ -96,10 +92,6 @@ class Gcp_Output_Options(Provider_Output_Options): else: self.output_filename = arguments.output_filename - # Remove HTML Output since it is not supported yet - if "html" in arguments.output_modes: - arguments.output_modes.remove("html") - class Aws_Output_Options(Provider_Output_Options): security_hub_enabled: bool diff --git a/prowler/providers/common/quick_inventory.py b/prowler/providers/common/quick_inventory.py index 49437b9d..de2fb7b8 100644 --- a/prowler/providers/common/quick_inventory.py +++ b/prowler/providers/common/quick_inventory.py @@ -7,7 +7,7 @@ from prowler.providers.aws.lib.quick_inventory.quick_inventory import quick_inve def run_provider_quick_inventory(provider, audit_info, output_directory): """ - run_provider_quick_inventory executes the quick inventory for te provider + run_provider_quick_inventory executes the quick inventory for the provider """ try: # Dynamically get the Provider quick inventory handler diff --git a/tests/providers/common/common_outputs_test.py b/tests/providers/common/common_outputs_test.py index 52fcd2f5..ab0cd1a3 100644 --- a/tests/providers/common/common_outputs_test.py +++ b/tests/providers/common/common_outputs_test.py @@ -3,6 +3,7 @@ from argparse import Namespace from boto3 import session from mock import patch +from prowler.lib.outputs.html import get_assessment_summary from prowler.providers.aws.lib.audit_info.audit_info import AWS_Audit_Info from prowler.providers.azure.lib.audit_info.audit_info import ( Azure_Audit_Info, @@ -11,8 +12,10 @@ from prowler.providers.azure.lib.audit_info.audit_info import ( from prowler.providers.common.outputs import ( Aws_Output_Options, Azure_Output_Options, + Gcp_Output_Options, set_provider_output_options, ) +from prowler.providers.gcp.lib.audit_info.models import GCP_Audit_Info AWS_ACCOUNT_NUMBER = "012345678912" DATETIME = "20230101120000" @@ -38,6 +41,16 @@ class Test_Common_Output_Options: ) return audit_info + # Mocked GCP Audit Info + def set_mocked_gcp_audit_info(self): + audit_info = GCP_Audit_Info( + credentials=None, + project_id="test-project", + audit_resources=None, + audit_metadata=None, + ) + return audit_info + # Mocked AWS Audit Info def set_mocked_aws_audit_info(self): audit_info = AWS_Audit_Info( @@ -48,9 +61,9 @@ class Test_Common_Output_Options: botocore_session=None, ), audited_account=AWS_ACCOUNT_NUMBER, - audited_user_id=None, + audited_user_id="test-user", audited_partition="aws", - audited_identity_arn=None, + audited_identity_arn="test-user-arn", profile=None, profile_region=None, credentials=None, @@ -91,6 +104,33 @@ class Test_Common_Output_Options: assert output_options.verbose assert output_options.output_filename == arguments.output_filename + def test_set_provider_output_options_gcp(self): + # Set the cloud provider + provider = "gcp" + # Set the arguments passed + arguments = Namespace() + arguments.quiet = True + arguments.output_modes = ["html", "csv", "json"] + arguments.output_directory = "output_test_directory" + arguments.verbose = True + arguments.output_filename = "output_test_filename" + arguments.only_logs = False + + audit_info = self.set_mocked_gcp_audit_info() + allowlist_file = "" + bulk_checks_metadata = {} + output_options = set_provider_output_options( + provider, arguments, audit_info, allowlist_file, bulk_checks_metadata + ) + assert isinstance(output_options, Gcp_Output_Options) + assert output_options.is_quiet + assert output_options.output_modes == ["html", "csv", "json"] + assert output_options.output_directory == arguments.output_directory + assert output_options.allowlist_file == "" + assert output_options.bulk_checks_metadata == {} + assert output_options.verbose + assert output_options.output_filename == arguments.output_filename + def test_set_provider_output_options_aws_no_output_filename(self): # Set the cloud provider provider = "aws" @@ -148,6 +188,7 @@ class Test_Common_Output_Options: assert isinstance(output_options, Azure_Output_Options) assert output_options.is_quiet assert output_options.output_modes == [ + "html", "csv", "json", ] @@ -184,6 +225,7 @@ class Test_Common_Output_Options: assert isinstance(output_options, Azure_Output_Options) assert output_options.is_quiet assert output_options.output_modes == [ + "html", "csv", "json", ] @@ -195,3 +237,132 @@ class Test_Common_Output_Options: output_options.output_filename == f"prowler-output-{'-'.join(tenants)}-{DATETIME}" ) + + def test_azure_get_assessment_summary(self): + # Mock Azure Audit Info + audit_info = self.set_mocked_azure_audit_info() + tenants = ["tenant-1", "tenant-2"] + audit_info.identity.tenant_ids = tenants + audit_info.identity.subscriptions = { + "Azure subscription 1": "12345-qwerty", + "Subscription2": "12345-qwerty", + } + printed_subscriptions = [] + for key, value in audit_info.identity.subscriptions.items(): + intermediate = key + " : " + value + printed_subscriptions.append(intermediate) + assert ( + get_assessment_summary(audit_info) + == f""" +
    +
    +
    + Azure Assessment Summary +
    +
      +
    • + Azure Tenant IDs: {" ".join(audit_info.identity.tenant_ids)} +
    • +
    • + Azure Tenant Domain: {audit_info.identity.domain} +
    • +
    • + Azure Subscriptions: {" ".join(printed_subscriptions)} +
    • +
    +
    +
    +
    +
    +
    + Azure Credentials +
    +
      +
    • + Azure Identity Type: {audit_info.identity.identity_type} +
    • +
    • + Azure Identity ID: {audit_info.identity.identity_id} +
    • +
    +
    +
    + """ + ) + + def test_aws_get_assessment_summary(self): + # Mock AWS Audit Info + audit_info = self.set_mocked_aws_audit_info() + + assert ( + get_assessment_summary(audit_info) + == f""" +
    +
    +
    + AWS Assessment Summary +
    +
      +
    • + AWS Account: {audit_info.audited_account} +
    • +
    • + AWS-CLI Profile: {audit_info.profile} +
    • +
    • + Audited Regions: All Regions +
    • +
    +
    +
    +
    +
    +
    + AWS Credentials +
    +
      +
    • + User Id: {audit_info.audited_user_id} +
    • +
    • + Caller Identity ARN: {audit_info.audited_identity_arn} +
    • +
    +
    +
    + """ + ) + + def test_gcp_get_assessment_summary(self): + # Mock Azure Audit Info + audit_info = self.set_mocked_gcp_audit_info() + profile = "default" + assert ( + get_assessment_summary(audit_info) + == f""" +
    +
    +
    + GCP Assessment Summary +
    +
      +
    • + GCP Project ID: {audit_info.project_id} +
    • +
    +
    +
    +
    +
    +
    + GCP Credentials +
    +
      +
    • + GCP Account: {profile} +
    • +
    +
    +
    + """ + )