From bf83f38c8979ae3f667eb817e5f31a5d59a48b00 Mon Sep 17 00:00:00 2001 From: Sergio Garcia <38561120+sergargar@users.noreply.github.com> Date: Thu, 4 Jul 2024 13:28:09 -0400 Subject: [PATCH] chore(html): add HTML class (#4360) --- prowler/__main__.py | 23 +- prowler/lib/outputs/asff/asff.py | 6 +- prowler/lib/outputs/csv/models.py | 6 +- prowler/lib/outputs/file_descriptors.py | 28 +- prowler/lib/outputs/html/html.py | 601 +++++++++--------- prowler/lib/outputs/ocsf/ocsf.py | 6 +- prowler/lib/outputs/output.py | 7 +- prowler/lib/outputs/outputs.py | 8 - tests/lib/outputs/html/html_test.py | 516 +++++++++++++++ tests/lib/outputs/outputs_test.py | 37 +- .../autoscaling/autoscaling_service_test.py | 3 +- ...policy_allows_privilege_escalation_test.py | 1 - ...policy_allows_privilege_escalation_test.py | 1 - tests/providers/gcp/gcp_fixtures.py | 3 +- .../kubernetes/kubernetes_provider_test.py | 1 - 15 files changed, 877 insertions(+), 370 deletions(-) create mode 100644 tests/lib/outputs/html/html_test.py 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""" +
{html.escape(finding.risk)}
{html.escape(finding.remediation_recommendation_text)}
{parse_html_string(unroll_dict(finding.compliance))}
{html.escape(finding.risk)}
{html.escape(finding.remediation_recommendation_text)}
{parse_html_string(unroll_dict(finding.compliance))}
test-risk
+•test-compliance: test-compliance +
test-risk
+•test-compliance: test-compliance +
test-risk
+•test-compliance: test-compliance +
test-risk
+•test-compliance: test-compliance +
+ | Status | +Severity | +Service Name | +Region | +Check ID | +Check Title | +Resource ID | +Resource Tags | +Status Extended | +Risk | +Recomendation | +Compliance | +
|---|