mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
chore(html): support markdown in HTML (#8727)
This commit is contained in:
Generated
+5
-5
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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("<", "<").replace(">", ">").replace("_", "<wbr />_")}</td>
|
||||
<td>{parse_html_string(unroll_dict(finding.resource_tags))}</td>
|
||||
<td>{finding.status_extended.replace("<", "<").replace(">", ">").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>
|
||||
"""
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user