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:
+
+
-
+
-
![]()

-
+
@@ -300,28 +1155,24 @@ class HTML(Output):
{HTML.get_assessment_summary(provider)}
-
+
-
- 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):
-
+
+
| Status |
@@ -338,22 +1190,34 @@ class HTML(Output):
Service Name |
Region |
Check ID |
- Check Title |
Resource ID |
- Resource Tags |
- Status Extended |
- Risk |
- Recommendation |
- Compliance |
+ Resource Type |
"""
)
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):
+
+
+
@@ -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 account: {provider.identity.account_name}
-
-
-
-
-
-
-
-
- -
- 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