From d5c6a7791cf380b1678f22d041789fc9869286ac Mon Sep 17 00:00:00 2001 From: pedrooot Date: Mon, 2 Jun 2025 10:13:06 +0200 Subject: [PATCH] feat(html): make some tweaks --- prowler/lib/outputs/html/html.py | 1662 +++++++++++++++++++++++++----- 1 file changed, 1415 insertions(+), 247 deletions(-) diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py index 4522f4d1bc..db86eeeeaa 100644 --- a/prowler/lib/outputs/html/html.py +++ b/prowler/lib/outputs/html/html.py @@ -1,4 +1,5 @@ import html +import json import sys from io import TextIOWrapper @@ -10,7 +11,6 @@ from prowler.config.config import ( ) from prowler.lib.logger import logger from prowler.lib.outputs.output import Finding, Output -from prowler.lib.outputs.utils import parse_html_string, unroll_dict from prowler.providers.common.provider import Provider @@ -24,70 +24,61 @@ class HTML(Output): """ try: for finding in findings: - # Badge for severity - severity = finding.metadata.Severity.value - if severity.upper() == "CRITICAL": - severity_badge = ' Critical' - elif severity.upper() == "HIGH": - severity_badge = ' High' - elif severity.upper() == "MEDIUM": - severity_badge = ' Medium' - elif severity.upper() == "LOW": - severity_badge = ' Low' - else: - severity_badge = f'{severity}' + row_class = "p-3 mb-2 bg-success-custom" + finding_status = finding.status.value + status_order = "2" # Default for PASS + if finding.status == "FAIL": + status_order = "1" + row_class = "table-danger" + elif finding.status == "MANUAL": + status_order = "3" + row_class = "table-info" - # Badge for status - status = finding.status.value.upper() + # Change the status of the finding if it's muted if finding.muted: - status_badge = 'Muted' - elif status == "FAIL": - status_badge = 'Fail' - elif status == "PASS": - status_badge = 'Pass' - elif status == "MANUAL": - status_badge = 'Manual' - else: - status_badge = f'{status}' + finding_status = f"MUTED ({finding_status})" + # Muted FAIL should still sort as FAIL, Muted PASS as PASS + if finding.status == "FAIL": # Original status before muting + row_class = "table-warning" # Muted FAIL is warning + # else: Muted PASS remains success-custom or its original class if not FAIL - # Chips for resource tags - tags_html = "" - tags = finding.resource_tags or {} - for k, v in tags.items(): - tags_html += f'{html.escape(str(k))}: {html.escape(str(v))}' - if not tags_html: - tags_html = "-" + severity_value = finding.metadata.Severity.value.lower() + severity_order = "5" # Default for informational or unknown + if severity_value == "critical": + severity_order = "1" + elif severity_value == "high": + severity_order = "2" + elif severity_value == "medium": + severity_order = "3" + elif severity_value == "low": + severity_order = "4" - # Chips for compliance - compliance_html = "" - compliance = finding.compliance or {} - if isinstance(compliance, dict): - for k, v in compliance.items(): - compliance_html += f'{html.escape(str(k))}: {html.escape(str(v))}' - elif isinstance(compliance, list): - for v in compliance: - compliance_html += f'{html.escape(str(v))}' - else: - if compliance: - compliance_html = f'{html.escape(str(compliance))}' - if not compliance_html: - compliance_html = "-" + # Prepare raw data for complex fields + raw_tags_json_string = json.dumps( + finding.resource_tags if finding.resource_tags else {} + ) + raw_compliance_json_string = json.dumps( + finding.compliance if finding.compliance else {} + ) self._data.append( f""" - - {status_badge} - {severity_badge} - {finding.metadata.ServiceName} - {finding.region.lower()} - {finding.metadata.CheckID.replace("_", "_")} - {finding.metadata.CheckTitle} - {finding.resource_uid.replace("<", "<").replace(">", ">").replace("_", "_")} - {tags_html} - {finding.status_extended.replace("<", "<").replace(">", ">").replace("_", "_")} -

{html.escape(finding.metadata.Risk)}

-

{html.escape(finding.metadata.Remediation.Recommendation.Text)}

- {compliance_html} + + {finding_status} + {finding.metadata.Severity.value} + {finding.metadata.ServiceName} + {finding.region.lower()} + {finding.metadata.CheckID.replace("_", "_")} + {finding.resource_uid.replace("<", "<").replace(">", ">").replace("_", "_")} + {finding.resource_type if finding.resource_type else ''} """ ) @@ -151,104 +142,961 @@ class HTML(Output): @@ -271,14 +1119,21 @@ class HTML(Output): Prowler - The Handy Cloud Security Tool +
+ Dark Mode: + +
-
+
- prowler-logo -
+
Report Information
@@ -300,28 +1155,24 @@ class HTML(Output):
{HTML.get_assessment_summary(provider)}
-
+
Assessment Overview
  • - Total Findings: {str(stats.get("findings_count", 0))} + Total Findings: {str(stats.get("findings_count", 0))} +
  • + {HTML._generate_overview_bar_list_item(stats, "total_pass", "Passed", "overview-bar-passed")} +
  • + Passed (Muted): {str(stats.get("total_muted_pass", 0))} +
  • + {HTML._generate_overview_bar_list_item(stats, "total_fail", "Failed", "overview-bar-failed")} +
  • + Failed (Muted): {str(stats.get("total_muted_fail", 0))}
  • - Passed: {str(stats.get("total_pass", 0))} -
  • -
  • - Passed (Muted): {str(stats.get("total_muted_pass", 0))} -
  • -
  • - Failed: {str(stats.get("total_fail", 0))} -
  • -
  • - Failed (Muted): {str(stats.get("total_muted_fail", 0))} -
  • -
  • - Total Resources: {str(stats.get("resources_count", 0))} + Total Resources: {str(stats.get("resources_count", 0))}
@@ -330,7 +1181,8 @@ class HTML(Output):
- +
+
@@ -338,22 +1190,34 @@ class HTML(Output): - - - - - - + """ ) except Exception as error: logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + @staticmethod + def _generate_overview_bar_list_item( + stats: dict, stat_key: str, label: str, bar_class: str + ) -> str: + total_findings = stats.get("findings_count", 0) + count = stats.get(stat_key, 0) + percentage = (count / total_findings * 100) if total_findings > 0 else 0 + return f""" +
  • + {label}: +
    + +
    + {str(count)} +
  • + """ + @staticmethod def write_footer(file_descriptor: TextIOWrapper) -> None: """ @@ -369,6 +1233,19 @@ class HTML(Output):
    StatusService Name Region Check IDCheck Title Resource IDResource TagsStatus ExtendedRiskRecommendationComplianceResource Type
    + +
    +
    +
    +

    Finding Details

    + +
    +
    + +
    +
    +
    + @@ -383,23 +1260,22 @@ class HTML(Output): @@ -676,55 +1899,11 @@ class HTML(Output): ) return "" - @staticmethod - def get_github_assessment_summary(provider: Provider) -> str: - """ - get_github_assessment_summary gets the HTML assessment summary for the provider - - Args: - provider (Provider): the provider object - - Returns: - str: the HTML assessment summary - """ - try: - return f""" -
    -
    -
    - GitHub Assessment Summary -
    -
      -
    • - GitHub account: {provider.identity.account_name} -
    • -
    -
    -
    -
    -
    -
    - GitHub Credentials -
    -
      -
    • - GitHub authentication method: {provider.auth_method} -
    • -
    -
    -
    """ - except Exception as error: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" - ) - return "" - @staticmethod def get_m365_assessment_summary(provider: Provider) -> str: """ get_m365_assessment_summary gets the HTML assessment summary for the provider + Args: provider (Provider): the provider object @@ -740,9 +1919,7 @@ class HTML(Output):
    • - M365 Tenant Domain: { - provider.identity.tenant_domain - } + M365 Tenant Domain: {provider.identity.tenant_domain}
    @@ -759,14 +1936,6 @@ class HTML(Output):
  • M365 Identity ID: {provider.identity.identity_id}
  • - { - f'''
  • - M365 User: {provider.identity.user} -
  • ''' - if hasattr(provider.identity, "user") - and provider.identity.user is not None - else "" - }
    """ @@ -837,7 +2006,6 @@ class HTML(Output): # It is not pretty but useful # AWS_provider --> aws # GCP_provider --> gcp - # GitHub_provider --> github # Azure_provider --> azure # Kubernetes_provider --> kubernetes