From 60e06dcc6ec987e9609bb8e53a4d226ea02b32a3 Mon Sep 17 00:00:00 2001
From: Sergio Garcia
Date: Mon, 15 Sep 2025 05:38:18 -0400
Subject: [PATCH] chore(html): support markdown in HTML (#8727)
---
poetry.lock | 10 ++--
prowler/CHANGELOG.md | 1 +
prowler/lib/outputs/html/html.py | 43 ++++++++++++++-
pyproject.toml | 1 +
tests/lib/outputs/html/html_test.py | 86 +++++++++++++++++++++++++++++
5 files changed, 133 insertions(+), 8 deletions(-)
diff --git a/poetry.lock b/poetry.lock
index 9d4fb31e02..def2321b2a 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -2530,14 +2530,14 @@ files = [
[[package]]
name = "markdown"
-version = "3.8.2"
+version = "3.9"
description = "Python implementation of John Gruber's Markdown."
optional = false
python-versions = ">=3.9"
-groups = ["docs"]
+groups = ["main", "docs"]
files = [
- {file = "markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24"},
- {file = "markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45"},
+ {file = "markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280"},
+ {file = "markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a"},
]
[package.dependencies]
@@ -5891,4 +5891,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">3.9.1,<3.13"
-content-hash = "aea38b0311bfabac00d4bf9ee5d2fa0a7f3e32dd2ee5c5d27eb54c69a80b35e9"
+content-hash = "285ee6b8c630e9908b8b05ced6be1cb67385d5f83af2b6175430a7ccdb9606a4"
diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md
index 5d873f663b..c5797f961d 100644
--- a/prowler/CHANGELOG.md
+++ b/prowler/CHANGELOG.md
@@ -11,6 +11,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
### Changed
- Update AWS Neptune service metadata to new format [(#8494)](https://github.com/prowler-cloud/prowler/pull/8494)
- Update AWS Config service metadata to new format [(#8641)](https://github.com/prowler-cloud/prowler/pull/8641)
+- HTML output now properly renders markdown syntax in Risk and Recommendation fields [(#8727)](https://github.com/prowler-cloud/prowler/pull/8727)
### Fixed
diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py
index c35d2826d1..903c9d7b91 100644
--- a/prowler/lib/outputs/html/html.py
+++ b/prowler/lib/outputs/html/html.py
@@ -1,7 +1,8 @@
-import html
import sys
from io import TextIOWrapper
+import markdown
+
from prowler.config.config import (
html_logo_url,
prowler_version,
@@ -15,6 +16,42 @@ from prowler.providers.common.provider import Provider
class HTML(Output):
+ @staticmethod
+ def process_markdown(text: str) -> str:
+ """
+ Process markdown syntax in text and convert to HTML using the markdown library.
+
+ Args:
+ text (str): Text containing markdown syntax
+
+ Returns:
+ str: HTML with markdown syntax converted
+ """
+ if not text:
+ return text
+
+ # Initialize markdown converter with safe mode to prevent XSS
+ md = markdown.Markdown(extensions=["nl2br"])
+
+ # Convert markdown to HTML
+ html_content = md.convert(text)
+
+ # Strip outer tags if present, as we're embedding in existing HTML
+ # Handle single paragraph case
+ if (
+ html_content.startswith("
")
+ and html_content.endswith("
")
+ and html_content.count("") == 1
+ ):
+ html_content = html_content[3:-4]
+ # Handle multiple paragraphs case - replace
and
with
+ elif "" in html_content and "
" in html_content:
+ html_content = html_content.replace("
\n", "
\n
\n")
+ html_content = html_content.replace("
", "")
+ html_content = html_content.replace("
", "")
+
+ return html_content
+
def transform(self, findings: list[Finding]) -> None:
"""Transforms the findings into the HTML format.
@@ -47,8 +84,8 @@ class HTML(Output):
{finding.resource_uid.replace("<", "<").replace(">", ">").replace("_", "_")} |
{parse_html_string(unroll_dict(finding.resource_tags))} |
{finding.status_extended.replace("<", "<").replace(">", ">").replace("_", "_")} |
- {html.escape(finding.metadata.Risk)} |
- {html.escape(finding.metadata.Remediation.Recommendation.Text)} |
+ {HTML.process_markdown(finding.metadata.Risk)} |
+ {HTML.process_markdown(finding.metadata.Remediation.Recommendation.Text)} |
{parse_html_string(unroll_dict(finding.compliance, separator=": "))} |
"""
diff --git a/pyproject.toml b/pyproject.toml
index 7d031c9b24..7e6125f131 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -51,6 +51,7 @@ dependencies = [
"google-auth-httplib2>=0.1,<0.3",
"jsonschema==4.23.0",
"kubernetes==32.0.1",
+ "markdown==3.9.0",
"microsoft-kiota-abstractions==1.9.2",
"msgraph-sdk==1.23.0",
"numpy==2.0.2",
diff --git a/tests/lib/outputs/html/html_test.py b/tests/lib/outputs/html/html_test.py
index 3747d2007a..12d2374a29 100644
--- a/tests/lib/outputs/html/html_test.py
+++ b/tests/lib/outputs/html/html_test.py
@@ -795,3 +795,89 @@ class TestHTML:
summary = output.get_assessment_summary(provider)
assert summary == mongodbatlas_html_assessment_summary
+
+ def test_process_markdown_bold_text(self):
+ """Test that **text** is converted to text"""
+ test_text = "This is **bold text** and this is **also bold**"
+ result = HTML.process_markdown(test_text)
+ expected = (
+ "This is bold text and this is also bold"
+ )
+ assert result == expected
+
+ def test_process_markdown_italic_text(self):
+ """Test that *text* is converted to text"""
+ test_text = "This is *italic text* and this is *also italic*"
+ result = HTML.process_markdown(test_text)
+ expected = "This is italic text and this is also italic"
+ assert result == expected
+
+ def test_process_markdown_code_text(self):
+ """Test that `text` is converted to text"""
+ test_text = "Use the `ls` command to list files and `cd` to change directories"
+ result = HTML.process_markdown(test_text)
+ expected = "Use the ls command to list files and cd to change directories"
+ assert result == expected
+
+ def test_process_markdown_line_breaks(self):
+ """Test that line breaks are converted to
tags"""
+ test_text = "Line 1\nLine 2\nLine 3"
+ result = HTML.process_markdown(test_text)
+ expected = "Line 1
\nLine 2
\nLine 3"
+ assert result == expected
+
+ def test_process_markdown_mixed_formatting(self):
+ """Test mixed markdown formatting"""
+ test_text = "**Bold text** with *italic* and `code` elements.\n\nNew paragraph with **more bold**."
+ result = HTML.process_markdown(test_text)
+ expected = "Bold text with italic and code elements.
\n
\nNew paragraph with more bold."
+ assert result == expected
+
+ def test_process_markdown_empty_string(self):
+ """Test that empty string returns empty string"""
+ result = HTML.process_markdown("")
+ assert result == ""
+
+ def test_process_markdown_none_input(self):
+ """Test that None input returns None"""
+ result = HTML.process_markdown(None)
+ assert result is None
+
+ def test_process_markdown_no_markdown(self):
+ """Test that plain text without markdown is returned unchanged"""
+ test_text = "This is plain text without any markdown formatting"
+ result = HTML.process_markdown(test_text)
+ assert result == test_text
+
+ def test_transform_with_markdown_risk(self):
+ """Test that Risk field with markdown is properly converted"""
+ findings = [
+ generate_finding_output(
+ risk="Outdated contacts delay **security notifications** and slow **incident response**",
+ remediation_recommendation_url="https://hub.prowler.com/check/check-id",
+ )
+ ]
+ html = HTML(findings)
+ output_data = html.data[0]
+
+ # Check that markdown is converted to HTML
+ assert "security notifications" in output_data
+ assert "incident response" in output_data
+
+ def test_transform_with_markdown_recommendation(self):
+ """Test that Recommendation field with markdown is properly converted"""
+ findings = [
+ generate_finding_output(
+ risk="test-risk",
+ remediation_recommendation_text="Adopt:\n- **Primary** and **alternate contacts**\n- Use `monitored aliases`",
+ remediation_recommendation_url="https://hub.prowler.com/check/check-id",
+ )
+ ]
+ html = HTML(findings)
+ output_data = html.data[0]
+
+ # Check that markdown is converted to HTML
+ assert "Primary" in output_data
+ assert "alternate contacts" in output_data
+ assert "monitored aliases" in output_data
+ assert "
" in output_data # Line breaks converted