diff --git a/prowler/__main__.py b/prowler/__main__.py index d7f1436719..966b340e2e 100644 --- a/prowler/__main__.py +++ b/prowler/__main__.py @@ -9,6 +9,7 @@ from colorama import Fore, Style from prowler.config.config import ( csv_file_suffix, get_available_compliance_frameworks, + html_file_suffix, json_asff_file_suffix, json_ocsf_file_suffix, ) @@ -45,7 +46,7 @@ from prowler.lib.outputs.asff.asff import ASFF from prowler.lib.outputs.compliance.compliance import display_compliance_table from prowler.lib.outputs.csv.models import CSV from prowler.lib.outputs.finding import Finding -from prowler.lib.outputs.html.html import add_html_footer, fill_html_overview_statistics +from prowler.lib.outputs.html.html import HTML from prowler.lib.outputs.ocsf.ocsf import OCSF from prowler.lib.outputs.outputs import extract_findings_statistics from prowler.lib.outputs.slack.slack import Slack @@ -338,15 +339,19 @@ def prowler(): ) json_finding.batch_write_data_to_file() if "html" in mode: - # TODO: generate HTML here - add_html_footer( - global_provider.output_options.output_filename, - global_provider.output_options.output_directory, + # Generate HTML Finding Object + filename = ( + f"{global_provider.output_options.output_directory}/" + f"{global_provider.output_options.output_filename}{html_file_suffix}" ) - fill_html_overview_statistics( - stats, - global_provider.output_options.output_filename, - global_provider.output_options.output_directory, + html_finding = HTML( + findings=finding_outputs, + create_file_descriptor=True, + file_path=filename, + ) + # Write HTML Finding Object to file + html_finding.batch_write_data_to_file( + provider=global_provider, stats=stats ) # Send output to S3 if needed (-B / -D) diff --git a/prowler/lib/outputs/asff/asff.py b/prowler/lib/outputs/asff/asff.py index 2f6e1f657f..5ae31fde45 100644 --- a/prowler/lib/outputs/asff/asff.py +++ b/prowler/lib/outputs/asff/asff.py @@ -127,7 +127,11 @@ class ASFF(Output): None """ try: - if self._file_descriptor and not self._file_descriptor.closed: + if ( + getattr(self, "_file_descriptor", None) + and not self._file_descriptor.closed + and self._data + ): # Write JSON opening/header [ self._file_descriptor.write("[") diff --git a/prowler/lib/outputs/csv/models.py b/prowler/lib/outputs/csv/models.py index d2720a53af..aac2acdd08 100644 --- a/prowler/lib/outputs/csv/models.py +++ b/prowler/lib/outputs/csv/models.py @@ -30,7 +30,11 @@ class CSV(Output): def batch_write_data_to_file(self) -> None: """Writes the findings to a file using the CSV format using the `Output._file_descriptor`.""" try: - if self._file_descriptor and not self._file_descriptor.closed: + if ( + getattr(self, "_file_descriptor", None) + and not self._file_descriptor.closed + and self._data + ): csv_writer = DictWriter( self._file_descriptor, fieldnames=self._data[0].keys(), diff --git a/prowler/lib/outputs/file_descriptors.py b/prowler/lib/outputs/file_descriptors.py index 0db4ed65c2..9357a97d54 100644 --- a/prowler/lib/outputs/file_descriptors.py +++ b/prowler/lib/outputs/file_descriptors.py @@ -2,7 +2,7 @@ from csv import DictWriter from io import TextIOWrapper from typing import Any -from prowler.config.config import csv_file_suffix, html_file_suffix +from prowler.config.config import csv_file_suffix from prowler.lib.logger import logger from prowler.lib.outputs.compliance.mitre_attack.models import ( MitreAttackAWS, @@ -20,7 +20,6 @@ from prowler.lib.outputs.compliance.models import ( Check_Output_CSV_KUBERNETES_CIS, ) from prowler.lib.outputs.csv.csv import generate_csv_fields -from prowler.lib.outputs.html.html import add_html_header from prowler.lib.outputs.output import Finding from prowler.lib.utils.utils import file_exists, open_file @@ -44,16 +43,13 @@ def initialize_file_descriptor( filename, "a", ) - if "html" in output_mode: - add_html_header(file_descriptor, provider) - else: - # Format is the class model of the CSV format to print the headers - csv_header = [x.upper() for x in generate_csv_fields(format)] - csv_writer = DictWriter( - file_descriptor, fieldnames=csv_header, delimiter=";" - ) - if write_header: - csv_writer.writeheader() + # Format is the class model of the CSV format to print the headers + csv_header = [x.upper() for x in generate_csv_fields(format)] + csv_writer = DictWriter( + file_descriptor, fieldnames=csv_header, delimiter=";" + ) + if write_header: + csv_writer.writeheader() return file_descriptor except Exception as error: logger.error( @@ -69,14 +65,12 @@ def fill_file_descriptors(output_modes, output_directory, output_filename, provi # FIXME: Remove this once we always use the new CSV(Output) if output_mode == "csv": continue + elif output_mode == "json-ocsf": + continue elif output_mode == "json-asff": continue elif output_mode == "html": - filename = f"{output_directory}/{output_filename}{html_file_suffix}" - file_descriptor = initialize_file_descriptor( - filename, output_mode, provider - ) - file_descriptors.update({output_mode: file_descriptor}) + continue elif provider.type == "gcp": filename = f"{output_directory}/compliance/{output_filename}_{output_mode}{csv_file_suffix}" diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py index c22cbb9d9a..759c44b4fb 100644 --- a/prowler/lib/outputs/html/html.py +++ b/prowler/lib/outputs/html/html.py @@ -1,27 +1,104 @@ import html -import importlib import sys -from os import path +from io import TextIOWrapper from prowler.config.config import ( - enconding_format_utf_8, - html_file_suffix, html_logo_url, prowler_version, square_logo_img, timestamp, ) from prowler.lib.logger import logger -from prowler.lib.outputs.output import Finding +from prowler.lib.outputs.output import Finding, Output from prowler.lib.outputs.utils import parse_html_string, unroll_dict -from prowler.lib.utils.utils import open_file +from prowler.providers.common.provider import Provider -def add_html_header(file_descriptor, provider): - try: - file_descriptor.write( - f""" - +class HTML(Output): + def transform(self, findings: list[Finding]) -> None: + """Transforms the findings into the HTML format. + + Args: + findings (list[Finding]): a list of Finding objects + + """ + try: + for finding in findings: + row_class = "p-3 mb-2 bg-success-custom" + finding_status = finding.status.value + # Change the status of the finding if it's muted + if finding.muted: + finding_status = f"MUTED ({finding_status})" + row_class = "table-warning" + if finding.status == "MANUAL": + row_class = "table-info" + elif finding.status == "FAIL": + row_class = "table-danger" + + self._data.append( + f""" + + {finding_status} + {finding.severity.value} + {finding.service_name} + {finding.region.lower()} + {finding.check_id.replace("_", "_")} + {finding.check_title} + {finding.resource_uid.replace("<", "<").replace(">", ">").replace("_", "_")} + {parse_html_string(finding.resource_tags)} + {finding.status_extended.replace("<", "<").replace(">", ">").replace("_", "_")} +

{html.escape(finding.risk)}

+

{html.escape(finding.remediation_recommendation_text)}

+

{parse_html_string(unroll_dict(finding.compliance))}

+ + """ + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + def batch_write_data_to_file(self, provider: Provider, stats: dict) -> None: + """ + Writes the findings to a file using the HTML format using the `Output._file_descriptor`. + + Args: + provider (Provider): the provider object + output_filename (str): the name of the output file + output_directory (str): the directory where the output file will be saved + stats (dict): the statistics of the findings + """ + try: + if ( + getattr(self, "_file_descriptor", None) + and not self._file_descriptor.closed + and self._data + ): + HTML.write_header(self._file_descriptor, provider, stats) + for finding in self._data: + self._file_descriptor.write(finding) + HTML.write_footer(self._file_descriptor) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + @staticmethod + def write_header( + file_descriptor: TextIOWrapper, provider: Provider, stats: dict + ) -> None: + """ + Writes the header of the HTML file. + + Args: + file_descriptor (file): the file descriptor to write the header + provider (Provider): the provider object + stats (dict): the statistics of the findings + """ + try: + file_descriptor.write( + f""" + @@ -81,7 +158,7 @@ def add_html_header(file_descriptor, provider): - {get_assessment_summary(provider)} + {HTML.get_assessment_summary(provider)}
@@ -89,16 +166,16 @@ def add_html_header(file_descriptor, provider):
@@ -124,107 +201,25 @@ def add_html_header(file_descriptor, provider): Compliance - - """ - ) - except Exception as error: - logger.critical( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - sys.exit(1) - - -def fill_html(file_descriptor, finding: Finding): - try: - row_class = "p-3 mb-2 bg-success-custom" - finding_status = finding.status.value - # Change the status of the finding if it's muted - if finding.muted: - finding_status = f"MUTED ({finding_status})" - row_class = "table-warning" - if finding.status == "MANUAL": - row_class = "table-info" - elif finding.status == "FAIL": - row_class = "table-danger" - - file_descriptor.write( - f""" - - {finding_status} - {finding.severity.value} - {finding.service_name} - {finding.region.lower()} - {finding.check_id.replace("_", "_")} - {finding.check_title} - {finding.resource_uid.replace("<", "<").replace(">", ">").replace("_", "_")} - {parse_html_string(finding.resource_tags)} - {finding.status_extended.replace("<", "<").replace(">", ">").replace("_", "_")} -

{html.escape(finding.risk)}

-

{html.escape(finding.remediation_recommendation_text)}

-

{parse_html_string(unroll_dict(finding.compliance))}

- - """ - ) - except Exception as error: - logger.critical( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - sys.exit(1) - - -def fill_html_overview_statistics(stats, output_filename, output_directory): - try: - filename = f"{output_directory}/{output_filename}{html_file_suffix}" - - # Read file - if path.isfile(filename): - with open(filename, "r", encoding=enconding_format_utf_8) as file: - filedata = file.read() - - # Replace statistics - # TOTAL_FINDINGS - filedata = filedata.replace( - "TOTAL_FINDINGS", str(stats.get("findings_count", 0)) + """ ) - # TOTAL_RESOURCES - filedata = filedata.replace( - "TOTAL_RESOURCES", str(stats.get("resources_count", 0)) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" ) - # TOTAL_PASS - filedata = filedata.replace("TOTAL_PASS", str(stats.get("total_pass", 0))) - # TOTAL_FAIL - filedata = filedata.replace("TOTAL_FAIL", str(stats.get("total_fail", 0))) - # Write file - with open(filename, "w", encoding=enconding_format_utf_8) as file: - file.write(filedata) + @staticmethod + def write_footer(file_descriptor: TextIOWrapper) -> None: + """ + Writes the footer of the HTML file. - except FileNotFoundError as error: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - except UnicodeDecodeError as error: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - except Exception as error: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - - -def add_html_footer(output_filename, output_directory): - try: - filename = f"{output_directory}/{output_filename}{html_file_suffix}" - # Close HTML file if exists - if path.isfile(filename): - file_descriptor = open_file( - filename, - "a", - ) + Args: + file_descriptor (file): the file descriptor to write the footer + """ + try: file_descriptor.write( """ - +
@@ -306,231 +301,261 @@ def add_html_footer(output_filename, output_directory): """ ) - file_descriptor.close() - except Exception as error: - logger.critical( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - sys.exit(1) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + @staticmethod + def get_aws_assessment_summary(provider: Provider) -> str: + """ + get_aws_assessment_summary gets the HTML assessment summary for the provider -def get_aws_html_assessment_summary(provider): - try: - if provider.__class__.__name__ == "AwsProvider": + Args: + provider (Provider): the provider object + + Returns: + str: the HTML assessment summary + """ + try: profile = ( - provider._identity.profile - if provider._identity.profile is not None + provider.identity.profile + if provider.identity.profile is not None else "default" ) - if isinstance(provider._identity.audited_regions, list): - audited_regions = " ".join(provider._identity.audited_regions) - elif not provider._identity.audited_regions: + if isinstance(provider.identity.audited_regions, list): + audited_regions = " ".join(provider.identity.audited_regions) + elif not provider.identity.audited_regions: audited_regions = "All Regions" else: - audited_regions = ", ".join(provider._identity.audited_regions) + audited_regions = ", ".join(provider.identity.audited_regions) return f""" -
+
+
+
+ AWS Assessment Summary +
+
    +
  • + AWS Account: {provider.identity.account} +
  • +
  • + AWS-CLI Profile: {profile} +
  • +
  • + Audited Regions: {audited_regions} +
  • +
+
+
+
- AWS Assessment Summary + AWS Credentials
  • - AWS Account: {provider._identity.account} -
  • -
  • - AWS-CLI Profile: {profile} -
  • -
  • - Audited Regions: {audited_regions} -
  • -
-
-
-
-
-
- AWS Credentials -
-
    -
  • - User Id: {provider._identity.user_id} -
  • -
  • - Caller Identity ARN: {provider._identity.identity_arn} -
  • -
-
-
- """ + User Id: {provider.identity.user_id} + +
  • + Caller Identity ARN: {provider.identity.identity_arn} +
  • + +
    + """ + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + return "" - except Exception as error: - logger.critical( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - sys.exit(1) + @staticmethod + def get_azure_assessment_summary(provider: Provider) -> str: + """ + get_azure_assessment_summary gets the HTML assessment summary for the provider + Args: + provider (Provider): the provider object -def get_azure_html_assessment_summary(provider): - try: - if provider.__class__.__name__ == "AzureProvider": + Returns: + str: the HTML assessment summary + """ + try: printed_subscriptions = [] - for key, value in provider._identity.subscriptions.items(): + for key, value in provider.identity.subscriptions.items(): intermediate = f"{key} : {value}" printed_subscriptions.append(intermediate) # check if identity is str(coming from SP) or dict(coming from browser or) - if isinstance(provider._identity.identity_id, dict): - html_identity = provider._identity.identity_id.get( + if isinstance(provider.identity.identity_id, dict): + html_identity = provider.identity.identity_id.get( "userPrincipalName", "Identity not found" ) else: - html_identity = provider._identity.identity_id + html_identity = provider.identity.identity_id return f""" -
    +
    +
    +
    + Azure Assessment Summary +
    +
      +
    • + Azure Tenant IDs: {" ".join(provider.identity.tenant_ids)} +
    • +
    • + Azure Tenant Domain: {provider.identity.tenant_domain} +
    • +
    • + Azure Subscriptions: {" ".join(printed_subscriptions)} +
    • +
    +
    +
    +
    - Azure Assessment Summary + Azure Credentials
    • - Azure Tenant IDs: {" ".join(provider._identity.tenant_ids)} -
    • -
    • - Azure Tenant Domain: {provider._identity.tenant_domain} -
    • -
    • - Azure Subscriptions: {" ".join(printed_subscriptions)} -
    • -
    -
    -
    -
    -
    -
    - Azure Credentials -
    -
      -
    • - Azure Identity Type: {provider._identity.identity_type} -
    • -
    • - Azure Identity ID: {html_identity} -
    • -
    -
    -
    - """ - except Exception as error: - logger.critical( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - sys.exit(1) + Azure Identity Type: {provider.identity.identity_type} + +
  • + Azure Identity ID: {html_identity} +
  • + +
    + """ + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + return "" + @staticmethod + def get_gcp_assessment_summary(provider: Provider) -> str: + """ + get_gcp_assessment_summary gets the HTML assessment summary for the provider -def get_gcp_html_assessment_summary(provider): - try: - if provider.__class__.__name__ == "GcpProvider": + Args: + provider (Provider): the provider object + + Returns: + str: the HTML assessment summary + """ + try: try: - getattr(provider.credentials, "_service_account_email") + getattr(provider.session, "_service_account_email") profile = ( - provider.credentials._service_account_email - if provider.credentials._service_account_email is not None + provider.session._service_account_email + if provider.session._service_account_email is not None else "default" ) except AttributeError: profile = "default" return f""" -
    -
    -
    - GCP Assessment Summary +
    +
    +
    + GCP Assessment Summary +
    +
      +
    • + GCP Project IDs: {", ".join(provider.project_ids)} +
    • +
    -
      -
    • - GCP Project IDs: {", ".join(provider.project_ids)} -
    • -
    -
    -
    -
    -
    - GCP Credentials +
    +
    +
    + GCP Credentials +
    +
      +
    • + GCP Account: {profile} +
    • +
    -
      -
    • - GCP Account: {profile} -
    • -
    -
    -
    - """ - except Exception as error: - logger.critical( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - sys.exit(1) +
    """ + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + return "" + @staticmethod + def get_kubernetes_assessment_summary(provider: Provider) -> str: + """ + get_kubernetes_assessment_summary gets the HTML assessment summary for the provider -def get_kubernetes_html_assessment_summary(provider): - try: - if provider.__class__.__name__ == "KubernetesProvider": + Args: + provider (Provider): the provider object + + Returns: + str: the HTML assessment summary + """ + try: return f""" -
    -
    -
    - Kubernetes Assessment Summary +
    +
    +
    + Kubernetes Assessment Summary +
    +
      +
    • + Kubernetes Cluster: {provider.identity.cluster} +
    • +
    -
      -
    • - Kubernetes Cluster: {provider._identity.cluster} -
    • -
    -
    -
    -
    -
    - Kubernetes Credentials +
    +
    +
    + Kubernetes Credentials +
    +
      +
    • + Kubernetes Context: {provider.identity.context} +
    • +
    -
      -
    • - Kubernetes Context: {provider._identity.context} -
    • -
    -
    -
    - """ - except Exception as error: - logger.critical( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - sys.exit(1) +
    """ + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + return "" + @staticmethod + def get_assessment_summary(provider: Provider) -> str: + """ + get_assessment_summary gets the HTML assessment summary for the provider -def get_assessment_summary(provider): - """ - get_assessment_summary gets the HTML assessment summary for the provider - """ - try: - # This is based in the Provider_provider class - # It is not pretty but useful - # AWS_provider --> aws - # GCP_provider --> gcp - # Azure_provider --> azure + Args: + provider (Provider): the provider object - # Dynamically get the Provider quick inventory handler - provider_html_assessment_summary_function = ( - f"get_{provider.type}_html_assessment_summary" - ) - return getattr( - importlib.import_module(__name__), provider_html_assessment_summary_function - )(provider) - except Exception as error: - logger.critical( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" - ) - sys.exit(1) + Returns: + str: the HTML assessment summary + """ + try: + # This is based in the Provider_provider class + # It is not pretty but useful + # AWS_provider --> aws + # GCP_provider --> gcp + # Azure_provider --> azure + # Kubernetes_provider --> kubernetes + + # Dynamically get the Provider quick inventory handler + provider_html_assessment_summary_function = ( + f"get_{provider.type}_assessment_summary" + ) + return getattr(HTML, provider_html_assessment_summary_function)(provider) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + return "" diff --git a/prowler/lib/outputs/ocsf/ocsf.py b/prowler/lib/outputs/ocsf/ocsf.py index 5fc5663aea..7a677f353a 100644 --- a/prowler/lib/outputs/ocsf/ocsf.py +++ b/prowler/lib/outputs/ocsf/ocsf.py @@ -168,7 +168,11 @@ class OCSF(Output): def batch_write_data_to_file(self) -> None: """Writes the findings to a file using the OCSF format using the `Output._file_descriptor`.""" try: - if self._file_descriptor and not self._file_descriptor.closed: + if ( + getattr(self, "_file_descriptor", None) + and not self._file_descriptor.closed + and self._data + ): self._file_descriptor.write("[") for finding in self._data: self._file_descriptor.write( diff --git a/prowler/lib/outputs/output.py b/prowler/lib/outputs/output.py index f9dfde3752..9abc267deb 100644 --- a/prowler/lib/outputs/output.py +++ b/prowler/lib/outputs/output.py @@ -34,9 +34,10 @@ class Output(ABC): file_path: str = None, ) -> None: self._data = [] - self.transform(findings) - if create_file_descriptor: - self.create_file_descriptor(file_path) + if findings: + self.transform(findings) + if create_file_descriptor: + self.create_file_descriptor(file_path) @property def data(self): diff --git a/prowler/lib/outputs/outputs.py b/prowler/lib/outputs/outputs.py index 252e6d730b..88c903ac42 100644 --- a/prowler/lib/outputs/outputs.py +++ b/prowler/lib/outputs/outputs.py @@ -7,8 +7,6 @@ from prowler.lib.outputs.compliance.compliance import ( fill_compliance, ) from prowler.lib.outputs.file_descriptors import fill_file_descriptors -from prowler.lib.outputs.finding import Finding -from prowler.lib.outputs.html.html import fill_html def stdout_report(finding, color, verbose, status, fix): @@ -92,12 +90,6 @@ def report(check_findings, provider): input_compliance_frameworks, ) - # Common Output Data - finding_output = Finding.generate_output(provider, finding) - - if "html" in file_descriptors: - fill_html(file_descriptors["html"], finding_output) - else: # No service resources in the whole account color = set_report_color("MANUAL") if output_options.verbose: diff --git a/tests/lib/outputs/html/html_test.py b/tests/lib/outputs/html/html_test.py new file mode 100644 index 0000000000..6c906b08d0 --- /dev/null +++ b/tests/lib/outputs/html/html_test.py @@ -0,0 +1,516 @@ +import sys +from io import StringIO + +from prowler.config.config import timestamp +from prowler.lib.outputs.html.html import HTML +from tests.lib.outputs.fixtures.fixtures import generate_finding_output +from tests.providers.aws.utils import AWS_REGION_EU_WEST_1, set_mocked_aws_provider +from tests.providers.azure.azure_fixtures import set_mocked_azure_provider +from tests.providers.gcp.gcp_fixtures import GCP_PROJECT_ID, set_mocked_gcp_provider +from tests.providers.kubernetes.kubernetes_fixtures import ( + set_mocked_kubernetes_provider, +) + +html_stats = { + "total_pass": 0, + "total_fail": 1, + "resources_count": 1, + "findings_count": 1, +} +pass_html_finding = """ + + PASS + high + test-service + eu-west-1 + test-check-id + test-check-id + + + +

    test-risk

    +

    +

    +•test-compliance: test-compliance +

    + + """ +fail_html_finding = """ + + FAIL + high + test-service + eu-west-1 + test-check-id + test-check-id + + + +

    test-risk

    +

    +

    +•test-compliance: test-compliance +

    + + """ +muted_html_finding = """ + + MUTED (PASS) + high + test-service + eu-west-1 + test-check-id + test-check-id + + + +

    test-risk

    +

    +

    +•test-compliance: test-compliance +

    + + """ +manual_html_finding = """ + + MANUAL + high + test-service + eu-west-1 + test-check-id + test-check-id + + + +

    test-risk

    +

    +

    +•test-compliance: test-compliance +

    + + """ +aws_html_assessment_summary = """ +
    +
    +
    + AWS Assessment Summary +
    +
      +
    • + AWS Account: 123456789012 +
    • +
    • + AWS-CLI Profile: default +
    • +
    • + Audited Regions: eu-west-1 +
    • +
    +
    +
    +
    +
    +
    + AWS Credentials +
    +
      +
    • + User Id: None +
    • +
    • + Caller Identity ARN: None +
    • +
    +
    +
    """ + +azure_html_assessment_summary = """ + +
    +
    +
    + Azure Assessment Summary +
    +
      +
    • + Azure Tenant IDs: 00000000-0000-0000-0000-000000000000 +
    • +
    • + Azure Tenant Domain: Unknown tenant domain (missing AAD permissions) +
    • +
    • + Azure Subscriptions: 4f647f43-15d2-4e3a-a7f0-8517cc4d977b : Subscription Name +
    • +
    +
    +
    +
    +
    +
    + Azure Credentials +
    +
      +
    • + Azure Identity Type: Service Principal +
    • +
    • + Azure Identity ID: 00000000-0000-0000-0000-000000000000 +
    • +
    +
    +
    """ + +gcp_html_assessment_summary = """ +
    +
    +
    + GCP Assessment Summary +
    +
      +
    • + GCP Project IDs: 123456789012 +
    • +
    +
    +
    +
    +
    +
    + GCP Credentials +
    +
      +
    • + GCP Account: test@test.com +
    • +
    +
    +
    """ + +kubernetes_html_assessment_summary = """ +
    +
    +
    + Kubernetes Assessment Summary +
    +
      +
    • + Kubernetes Cluster: None +
    • +
    +
    +
    +
    +
    +
    + Kubernetes Credentials +
    +
      +
    • + Kubernetes Context: None +
    • +
    +
    +
    """ + +aws_html_header = ( + """ + + + + + + + + + + + + + + Prowler - The Handy Cloud Security Tool + + +
    +
    +
    + prowler-logo +
    +
    + Report Information +
    +
      +
    • +
      +
      + Version: 4.2.4 +
      +
      +
    • +
    • + Parameters used: """ + + " ".join(sys.argv[1:]) + + """ +
    • +
    • + Date: """ + + timestamp.isoformat() + + """ +
    • +
    +
    +
    """ + + aws_html_assessment_summary + + """ +
    +
    +
    + Assessment Overview +
    +
      +
    • + Total Findings: 1 +
    • +
    • + Passed: 0 +
    • +
    • + Failed: 1 +
    • +
    • + Total Resources: 1 +
    • +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + """ +) + +html_footer = """ + +
    StatusSeverityService NameRegionCheck IDCheck TitleResource IDResource TagsStatus ExtendedRiskRecomendationCompliance
    +
    +
    + + + + + + + + + + + +""" + + +class TestHTML: + def test_transform_fail_finding(self): + findings = [generate_finding_output(status="FAIL")] + html = HTML(findings) + output_data = html.data[0] + assert isinstance(output_data, str) + assert output_data == fail_html_finding + + def test_transform_pass_finding(self): + findings = [generate_finding_output()] + html = HTML(findings) + output_data = html.data[0] + assert isinstance(output_data, str) + assert output_data == pass_html_finding + + def test_transform_muted_finding(self): + findings = [generate_finding_output(muted=True)] + html = HTML(findings) + output_data = html.data[0] + assert isinstance(output_data, str) + assert output_data == muted_html_finding + + def test_transform_manual_finding(self): + findings = [generate_finding_output(status="MANUAL")] + html = HTML(findings) + output_data = html.data[0] + assert isinstance(output_data, str) + assert output_data == manual_html_finding + + def test_batch_write_data_to_file(self): + mock_file = StringIO() + findings = [generate_finding_output()] + output = HTML(findings) + output._file_descriptor = mock_file + provider = set_mocked_aws_provider(audited_regions=[AWS_REGION_EU_WEST_1]) + + output.batch_write_data_to_file(provider, html_stats) + + mock_file.seek(0) + content = mock_file.read() + assert content == aws_html_header + pass_html_finding + html_footer + + def test_write_header(self): + mock_file = StringIO() + findings = [generate_finding_output()] + output = HTML(findings) + output._file_descriptor = mock_file + provider = set_mocked_aws_provider(audited_regions=[AWS_REGION_EU_WEST_1]) + + output.write_header(mock_file, provider, html_stats) + + mock_file.seek(0) + content = mock_file.read() + assert content == aws_html_header + + def test_write_footer(self): + mock_file = StringIO() + findings = [generate_finding_output()] + output = HTML(findings) + output._file_descriptor = mock_file + + output.write_footer(mock_file) + + mock_file.seek(0) + content = mock_file.read() + assert content == html_footer + + def test_aws_get_assessment_summary(self): + findings = [generate_finding_output()] + output = HTML(findings) + provider = set_mocked_aws_provider(audited_regions=[AWS_REGION_EU_WEST_1]) + + summary = output.get_assessment_summary(provider) + + assert summary == aws_html_assessment_summary + + def test_azure_get_assessment_summary(self): + findings = [generate_finding_output()] + output = HTML(findings) + provider = set_mocked_azure_provider() + + summary = output.get_assessment_summary(provider) + + assert summary == summary + + def test_gcp_get_assessment_summary(self): + findings = [generate_finding_output()] + output = HTML(findings) + provider = set_mocked_gcp_provider(project_ids=[GCP_PROJECT_ID]) + + summary = output.get_assessment_summary(provider) + + assert summary == gcp_html_assessment_summary + + def test_kubernetes_get_assessment_summary(self): + findings = [generate_finding_output()] + output = HTML(findings) + provider = set_mocked_kubernetes_provider() + + summary = output.get_assessment_summary(provider) + + assert summary == kubernetes_html_assessment_summary diff --git a/tests/lib/outputs/outputs_test.py b/tests/lib/outputs/outputs_test.py index 58a9ec510b..b4c12ea62e 100644 --- a/tests/lib/outputs/outputs_test.py +++ b/tests/lib/outputs/outputs_test.py @@ -1,11 +1,9 @@ -import os -from os import path, remove +from os import path from unittest import mock import pytest from colorama import Fore -from prowler.config.config import html_file_suffix, output_file_timestamp from prowler.lib.check.compliance_models import ( CIS_Requirement_Attribute, Compliance_Base_Model, @@ -14,7 +12,6 @@ from prowler.lib.check.compliance_models import ( from prowler.lib.check.models import Check_Report, load_check_metadata from prowler.lib.outputs.compliance.compliance import get_check_compliance from prowler.lib.outputs.csv.csv import generate_csv_fields -from prowler.lib.outputs.file_descriptors import fill_file_descriptors from prowler.lib.outputs.finding import Finding from prowler.lib.outputs.outputs import extract_findings_statistics, set_report_color from prowler.lib.outputs.utils import ( @@ -25,41 +22,9 @@ from prowler.lib.outputs.utils import ( unroll_list, unroll_tags, ) -from prowler.lib.utils.utils import open_file -from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, set_mocked_aws_provider class TestOutputs: - def test_fill_file_descriptors_aws(self): - audited_account = AWS_ACCOUNT_NUMBER - output_directory = f"{os.path.dirname(os.path.realpath(__file__))}" - aws_provider = set_mocked_aws_provider() - test_output_modes = [ - ["html"], - ] - output_filename = f"prowler-output-{audited_account}-{output_file_timestamp}" - expected = [ - { - "html": open_file( - f"{output_directory}/{output_filename}{html_file_suffix}", - "a", - ) - }, - ] - - for index, output_mode_list in enumerate(test_output_modes): - test_output_file_descriptors = fill_file_descriptors( - output_mode_list, - output_directory, - output_filename, - aws_provider, - ) - for output_mode in output_mode_list: - assert ( - test_output_file_descriptors[output_mode].name - == expected[index][output_mode].name - ) - remove(expected[index][output_mode].name) def test_set_report_color(self): test_status = ["PASS", "FAIL", "MANUAL"] diff --git a/tests/providers/aws/services/autoscaling/autoscaling_service_test.py b/tests/providers/aws/services/autoscaling/autoscaling_service_test.py index fe85a0725d..dbc4f05381 100644 --- a/tests/providers/aws/services/autoscaling/autoscaling_service_test.py +++ b/tests/providers/aws/services/autoscaling/autoscaling_service_test.py @@ -94,7 +94,7 @@ class Test_AutoScaling_Service: KeyName="the_keys", SecurityGroups=["default", "default2"], ) - asg = autoscaling_client.create_auto_scaling_group( + _ = autoscaling_client.create_auto_scaling_group( AutoScalingGroupName="my-autoscaling-group", LaunchConfigurationName="test", MinSize=0, @@ -112,7 +112,6 @@ class Test_AutoScaling_Service: # AutoScaling client for this test class aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) autoscaling = AutoScaling(aws_provider) - print("asg", asg) assert len(autoscaling.groups) == 1 # create_auto_scaling_group doesn't return the ARN, can't check it # assert autoscaling.groups[0].arn == diff --git a/tests/providers/aws/services/iam/iam_inline_policy_allows_privilege_escalation/iam_inline_policy_allows_privilege_escalation_test.py b/tests/providers/aws/services/iam/iam_inline_policy_allows_privilege_escalation/iam_inline_policy_allows_privilege_escalation_test.py index 4e5162334a..00e4c57b1e 100644 --- a/tests/providers/aws/services/iam/iam_inline_policy_allows_privilege_escalation/iam_inline_policy_allows_privilege_escalation_test.py +++ b/tests/providers/aws/services/iam/iam_inline_policy_allows_privilege_escalation/iam_inline_policy_allows_privilege_escalation_test.py @@ -516,7 +516,6 @@ class Test_iam_inline_policy_allows_privilege_escalation: policy_name = "privileged_policy" for values in privilege_escalation_policies_combination.values(): - print(list(values)) # We create a new statement in each loop with the combinations required to allow the privilege escalation policy_document = { "Version": "2012-10-17", diff --git a/tests/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation_test.py b/tests/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation_test.py index ea004609c0..41fe22aaca 100644 --- a/tests/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation_test.py +++ b/tests/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation_test.py @@ -454,7 +454,6 @@ class Test_iam_policy_allows_privilege_escalation: iam_client = client("iam", region_name=AWS_REGION_US_EAST_1) policy_name = "privileged_policy" for values in privilege_escalation_policies_combination.values(): - print(list(values)) # We create a new statement in each loop with the combinations required to allow the privilege escalation policy_document = { "Version": "2012-10-17", diff --git a/tests/providers/gcp/gcp_fixtures.py b/tests/providers/gcp/gcp_fixtures.py index 4ffb13e918..4968fc77b3 100644 --- a/tests/providers/gcp/gcp_fixtures.py +++ b/tests/providers/gcp/gcp_fixtures.py @@ -13,7 +13,8 @@ def set_mocked_gcp_provider( ) -> GcpProvider: provider = MagicMock() provider.type = "gcp" - provider.session = None + provider.session = MagicMock() + provider.session._service_account_email = "test@test.com" provider.project_ids = project_ids provider.identity = GCPIdentityInfo( profile=profile, diff --git a/tests/providers/kubernetes/kubernetes_provider_test.py b/tests/providers/kubernetes/kubernetes_provider_test.py index 5579a9bc50..ba714ee51a 100644 --- a/tests/providers/kubernetes/kubernetes_provider_test.py +++ b/tests/providers/kubernetes/kubernetes_provider_test.py @@ -55,7 +55,6 @@ class TestKubernetesProvider: # Instantiate the KubernetesProvider with mocked arguments kubernetes_provider = KubernetesProvider(arguments) - print(kubernetes_provider.__dict__) assert isinstance(kubernetes_provider.session, KubernetesSession) assert kubernetes_provider.session.api_client is not None assert kubernetes_provider.session.context == {