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