chore(html): support markdown in HTML (#8727)

This commit is contained in:
Sergio Garcia
2025-09-15 05:38:18 -04:00
committed by GitHub
parent 7733aab088
commit 60e06dcc6e
5 changed files with 133 additions and 8 deletions
Generated
+5 -5
View File
@@ -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"
+1
View File
@@ -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
+40 -3
View File
@@ -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 <p> tags if present, as we're embedding in existing HTML
# Handle single paragraph case
if (
html_content.startswith("<p>")
and html_content.endswith("</p>")
and html_content.count("<p>") == 1
):
html_content = html_content[3:-4]
# Handle multiple paragraphs case - replace <p> and </p> with <br><br>
elif "<p>" in html_content and "</p>" in html_content:
html_content = html_content.replace("</p>\n<p>", "<br />\n<br />\n")
html_content = html_content.replace("<p>", "")
html_content = html_content.replace("</p>", "")
return html_content
def transform(self, findings: list[Finding]) -> None:
"""Transforms the findings into the HTML format.
@@ -47,8 +84,8 @@ class HTML(Output):
<td>{finding.resource_uid.replace("<", "&lt;").replace(">", "&gt;").replace("_", "<wbr />_")}</td>
<td>{parse_html_string(unroll_dict(finding.resource_tags))}</td>
<td>{finding.status_extended.replace("<", "&lt;").replace(">", "&gt;").replace("_", "<wbr />_")}</td>
<td><p class="show-read-more">{html.escape(finding.metadata.Risk)}</p></td>
<td><p class="show-read-more">{html.escape(finding.metadata.Remediation.Recommendation.Text)}</p> <a class="read-more" href="{finding.metadata.Remediation.Recommendation.Url}"><i class="fas fa-external-link-alt"></i></a></td>
<td><p class="show-read-more">{HTML.process_markdown(finding.metadata.Risk)}</p></td>
<td><p class="show-read-more">{HTML.process_markdown(finding.metadata.Remediation.Recommendation.Text)}</p> <a class="read-more" href="{finding.metadata.Remediation.Recommendation.Url}"><i class="fas fa-external-link-alt"></i></a></td>
<td><p class="show-read-more">{parse_html_string(unroll_dict(finding.compliance, separator=": "))}</p></td>
</tr>
"""
+1
View File
@@ -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",
+86
View File
@@ -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 <strong>text</strong>"""
test_text = "This is **bold text** and this is **also bold**"
result = HTML.process_markdown(test_text)
expected = (
"This is <strong>bold text</strong> and this is <strong>also bold</strong>"
)
assert result == expected
def test_process_markdown_italic_text(self):
"""Test that *text* is converted to <em>text</em>"""
test_text = "This is *italic text* and this is *also italic*"
result = HTML.process_markdown(test_text)
expected = "This is <em>italic text</em> and this is <em>also italic</em>"
assert result == expected
def test_process_markdown_code_text(self):
"""Test that `text` is converted to <code>text</code>"""
test_text = "Use the `ls` command to list files and `cd` to change directories"
result = HTML.process_markdown(test_text)
expected = "Use the <code>ls</code> command to list files and <code>cd</code> to change directories"
assert result == expected
def test_process_markdown_line_breaks(self):
"""Test that line breaks are converted to <br> tags"""
test_text = "Line 1\nLine 2\nLine 3"
result = HTML.process_markdown(test_text)
expected = "Line 1<br />\nLine 2<br />\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 = "<strong>Bold text</strong> with <em>italic</em> and <code>code</code> elements.<br />\n<br />\nNew paragraph with <strong>more bold</strong>."
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 "<strong>security notifications</strong>" in output_data
assert "<strong>incident response</strong>" 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 "<strong>Primary</strong>" in output_data
assert "<strong>alternate contacts</strong>" in output_data
assert "<code>monitored aliases</code>" in output_data
assert "<br />" in output_data # Line breaks converted