diff --git a/dojo/templates/dojo/endpoint_pdf_report.html b/dojo/templates/dojo/endpoint_pdf_report.html index ef187bb1b76..b6e6ded2948 100644 --- a/dojo/templates/dojo/endpoint_pdf_report.html +++ b/dojo/templates/dojo/endpoint_pdf_report.html @@ -178,15 +178,15 @@
{% if finding.cvssv3 %}
CVSS v3
-
{{ finding.cvssv3|markdown_render }}
+
{{ finding.cvssv3|markdown_render }}
{% endif %}
Description
-
{{ finding.description|markdown_render }}
+
{{ finding.description|markdown_render }}
{% if finding.mitigation %}
Mitigation
-
{{ finding.mitigation|markdown_render }}
+
{{ finding.mitigation|markdown_render }}
{% endif %} {% if finding.get_report_requests %} @@ -203,22 +203,22 @@
Response {{forloop.counter}}
{% if finding.impact %}
Impact
-
{{ finding.impact|markdown_render }}
+
{{ finding.impact|markdown_render }}
{% endif %} {% if finding.steps_to_reproduce %}
Steps to Reproduce
-
{{ finding.steps_to_reproduce|markdown_render }}
+
{{ finding.steps_to_reproduce|markdown_render }}
{% endif %} {% if finding.severity_justification %}
Severity Justification
-
{{ finding.severity_justification|markdown_render }}
+
{{ finding.severity_justification|markdown_render }}
{% endif %} {% if finding.references %}
References
-
{{ finding.references|markdown_render }}
+
{{ finding.references|markdown_render }}
{% endif %} {% if include_finding_images %} diff --git a/dojo/templates/dojo/engagement_pdf_report.html b/dojo/templates/dojo/engagement_pdf_report.html index bb10597d7e9..f574a195cac 100644 --- a/dojo/templates/dojo/engagement_pdf_report.html +++ b/dojo/templates/dojo/engagement_pdf_report.html @@ -313,15 +313,15 @@
{% if finding.cvssv3 %}
CVSS v3
-
{{ finding.cvssv3|markdown_render }}
+
{{ finding.cvssv3|markdown_render }}
{% endif %}
Description
-
{{ finding.description|markdown_render }}
+
{{ finding.description|markdown_render }}
{% if finding.mitigation %}
Mitigation
-
{{ finding.mitigation|markdown_render }}
+
{{ finding.mitigation|markdown_render }}
{% endif %} {% if finding.get_report_requests %} @@ -338,22 +338,22 @@
Response {{forloop.counter}}
{% if finding.impact %}
Impact
-
{{ finding.impact|markdown_render }}
+
{{ finding.impact|markdown_render }}
{% endif %} {% if finding.steps_to_reproduce %}
Steps to Reproduce
-
{{ finding.steps_to_reproduce|markdown_render }}
+
{{ finding.steps_to_reproduce|markdown_render }}
{% endif %} {% if finding.severity_justification %}
Severity Justification
-
{{ finding.severity_justification|markdown_render }}
+
{{ finding.severity_justification|markdown_render }}
{% endif %} {% if finding.references %}
References
-
{{ finding.references|markdown_render }}
+
{{ finding.references|markdown_render }}
{% endif %} {% if include_finding_images %} diff --git a/dojo/templates/dojo/finding_pdf_report.html b/dojo/templates/dojo/finding_pdf_report.html index a40739d041b..e82dcb82a3d 100644 --- a/dojo/templates/dojo/finding_pdf_report.html +++ b/dojo/templates/dojo/finding_pdf_report.html @@ -157,15 +157,15 @@
{% if finding.cvssv3 %}
CVSS v3
-
{{ finding.cvssv3|markdown_render }}
+
{{ finding.cvssv3|markdown_render }}
{% endif %}
Description
-
{{ finding.description|markdown_render }}
+
{{ finding.description|markdown_render }}
{% if finding.mitigation %}
Mitigation
-
{{ finding.mitigation|markdown_render }}
+
{{ finding.mitigation|markdown_render }}
{% endif %} {% if finding.get_report_requests %} @@ -182,22 +182,22 @@
Response {{forloop.counter}}
{% if finding.impact %}
Impact
-
{{ finding.impact|markdown_render }}
+
{{ finding.impact|markdown_render }}
{% endif %} {% if finding.steps_to_reproduce %}
Steps to Reproduce
-
{{ finding.steps_to_reproduce|markdown_render }}
+
{{ finding.steps_to_reproduce|markdown_render }}
{% endif %} {% if finding.severity_justification %}
Severity Justification
-
{{ finding.severity_justification|markdown_render }}
+
{{ finding.severity_justification|markdown_render }}
{% endif %} {% if finding.references %}
References
-
{{ finding.references|markdown_render }}
+
{{ finding.references|markdown_render }}
{% endif %} {% if include_finding_images %} diff --git a/dojo/templates/dojo/product_endpoint_pdf_report.html b/dojo/templates/dojo/product_endpoint_pdf_report.html index f7de27fe7a3..6da95cd5bbd 100644 --- a/dojo/templates/dojo/product_endpoint_pdf_report.html +++ b/dojo/templates/dojo/product_endpoint_pdf_report.html @@ -227,15 +227,15 @@
{% if finding.cvssv3 %}
CVSS v3
-
{{ finding.cvssv3|markdown_render }}
+
{{ finding.cvssv3|markdown_render }}
{% endif %}
Description
-
{{ finding.description|markdown_render }}
+
{{ finding.description|markdown_render }}
{% if finding.mitigation %}
Mitigation
-
{{ finding.mitigation|markdown_render }}
+
{{ finding.mitigation|markdown_render }}
{% endif %} {% if finding.get_report_requests %} @@ -252,22 +252,22 @@
Response {{forloop.counter}}
{% if finding.impact %}
Impact
-
{{ finding.impact|markdown_render }}
+
{{ finding.impact|markdown_render }}
{% endif %} {% if finding.steps_to_reproduce %}
Steps to Reproduce
-
{{ finding.steps_to_reproduce|markdown_render }}
+
{{ finding.steps_to_reproduce|markdown_render }}
{% endif %} {% if finding.severity_justification %}
Severity Justification
-
{{ finding.severity_justification|markdown_render }}
+
{{ finding.severity_justification|markdown_render }}
{% endif %} {% if finding.references %}
References
-
{{ finding.references|markdown_render }}
+
{{ finding.references|markdown_render }}
{% endif %} {% if include_finding_images %} diff --git a/dojo/templates/dojo/product_pdf_report.html b/dojo/templates/dojo/product_pdf_report.html index 9768c006888..df1cf7b1b26 100644 --- a/dojo/templates/dojo/product_pdf_report.html +++ b/dojo/templates/dojo/product_pdf_report.html @@ -283,15 +283,15 @@
{% if finding.cvssv3 %}
CVSS v3
-
{{ finding.cvssv3|markdown_render }}
+
{{ finding.cvssv3|markdown_render }}
{% endif %}
Description
-
{{ finding.description|markdown_render }}
+
{{ finding.description|markdown_render }}
{% if finding.mitigation %}
Mitigation
-
{{ finding.mitigation|markdown_render }}
+
{{ finding.mitigation|markdown_render }}
{% endif %} {% if finding.get_report_requests %} @@ -308,22 +308,22 @@
Response {{forloop.counter}}
{% if finding.impact %}
Impact
-
{{ finding.impact|markdown_render }}
+
{{ finding.impact|markdown_render }}
{% endif %} {% if finding.steps_to_reproduce %}
Steps to Reproduce
-
{{ finding.steps_to_reproduce|markdown_render }}
+
{{ finding.steps_to_reproduce|markdown_render }}
{% endif %} {% if finding.severity_justification %}
Severity Justification
-
{{ finding.severity_justification|markdown_render }}
+
{{ finding.severity_justification|markdown_render }}
{% endif %} {% if finding.references %}
References
-
{{ finding.references|markdown_render }}
+
{{ finding.references|markdown_render }}
{% endif %} {% if include_finding_images %} diff --git a/dojo/templates/dojo/product_type_pdf_report.html b/dojo/templates/dojo/product_type_pdf_report.html index 483d024edc7..0a623ee3637 100644 --- a/dojo/templates/dojo/product_type_pdf_report.html +++ b/dojo/templates/dojo/product_type_pdf_report.html @@ -212,15 +212,15 @@
{% if finding.cvssv3 %}
CVSS v3
-
{{ finding.cvssv3|markdown_render }}
+
{{ finding.cvssv3|markdown_render }}
{% endif %}
Description
-
{{ finding.description|markdown_render }}
+
{{ finding.description|markdown_render }}
{% if finding.mitigation %}
Mitigation
-
{{ finding.mitigation|markdown_render }}
+
{{ finding.mitigation|markdown_render }}
{% endif %} {% if finding.get_report_requests %} @@ -237,22 +237,22 @@
Response {{forloop.counter}}
{% if finding.impact %}
Impact
-
{{ finding.impact|markdown_render }}
+
{{ finding.impact|markdown_render }}
{% endif %} {% if finding.steps_to_reproduce %}
Steps to Reproduce
-
{{ finding.steps_to_reproduce|markdown_render }}
+
{{ finding.steps_to_reproduce|markdown_render }}
{% endif %} {% if finding.severity_justification %}
Severity Justification
-
{{ finding.severity_justification|markdown_render }}
+
{{ finding.severity_justification|markdown_render }}
{% endif %} {% if finding.references %}
References
-
{{ finding.references|markdown_render }}
+
{{ finding.references|markdown_render }}
{% endif %} {% if include_finding_images %} diff --git a/dojo/templates/dojo/test_pdf_report.html b/dojo/templates/dojo/test_pdf_report.html index 17af82137bc..986ec8fccaa 100644 --- a/dojo/templates/dojo/test_pdf_report.html +++ b/dojo/templates/dojo/test_pdf_report.html @@ -325,15 +325,15 @@
{% if finding.cvssv3 %}
CVSS v3
-
{{ finding.cvssv3|markdown_render }}
+
{{ finding.cvssv3|markdown_render }}
{% endif %}
Description
-
{{ finding.description|markdown_render }}
+
{{ finding.description|markdown_render }}
{% if finding.mitigation %}
Mitigation
-
{{ finding.mitigation|markdown_render }}
+
{{ finding.mitigation|markdown_render }}
{% endif %} {% if finding.get_report_requests %} @@ -350,22 +350,22 @@
Response {{forloop.counter}}
{% if finding.impact %}
Impact
-
{{ finding.impact|markdown_render }}
+
{{ finding.impact|markdown_render }}
{% endif %} {% if finding.steps_to_reproduce %}
Steps to Reproduce
-
{{ finding.steps_to_reproduce|markdown_render }}
+
{{ finding.steps_to_reproduce|markdown_render }}
{% endif %} {% if finding.severity_justification %}
Severity Justification
-
{{ finding.severity_justification|markdown_render }}
+
{{ finding.severity_justification|markdown_render }}
{% endif %} {% if finding.references %}
References
-
{{ finding.references|markdown_render }}
+
{{ finding.references|markdown_render }}
{% endif %} {% if include_finding_images %} diff --git a/dojo/templates/report_base.html b/dojo/templates/report_base.html index a27264a0038..a3328eaadba 100644 --- a/dojo/templates/report_base.html +++ b/dojo/templates/report_base.html @@ -134,6 +134,24 @@ padding: 0 !important; overflow: visible !important; word-break: normal !important; + overflow-wrap: break-word !important; + } + + .report-field { + margin-bottom: 15px !important; + overflow-wrap: break-word !important; + word-break: normal !important; + } + + .report-field pre { + white-space: pre-wrap !important; + padding: 0 !important; + border: 0 !important; + background-color: transparent !important; + overflow: visible !important; + overflow-wrap: break-word !important; + word-break: normal !important; + margin: 0 !important; } ul{ diff --git a/unittests/test_pdf_report_rendering.py b/unittests/test_pdf_report_rendering.py new file mode 100644 index 00000000000..283fd4f6272 --- /dev/null +++ b/unittests/test_pdf_report_rendering.py @@ -0,0 +1,223 @@ +from django.template import engines +from django.utils.timezone import now + +from dojo.models import ( + Engagement, + Finding, + Product, + Product_Type, + Test, + Test_Type, + User, +) +from unittests.dojo_test_case import DojoTestCase + + +class TestPdfReportTextWrapping(DojoTestCase): + + """ + Tests that PDF report templates render long and pre-wrapped content + within margins instead of overflowing. + """ + + fixtures = ["dojo_testdata.json"] + + LONG_URL = "https://app.example.com/assets/vendor-" + "a1b2c3d4" * 8 + ".js.map" + + # Content with an embedded
 tag, simulating imports (e.g. BugCrowd CSV)
+    # that store HTML-wrapped text in finding fields.
+    DESCRIPTION_WITH_PRE = (
+        "
\n"
+        "An internal debug configuration file (debug-config-e7f3a901.json) is publicly "
+        "accessible at the URL: " + LONG_URL + ". "
+        "Debug configuration files can reveal internal service addresses, feature flags, "
+        "and environment variables. Exposing such files can leak sensitive information "
+        "about the application infrastructure, aiding attackers in lateral movement and "
+        "facilitating exploitation of internal services.\n"
+        "
" + ) + + MITIGATION_WITH_PRE = ( + '
\n'
+        "Remove Debug Files From Public Directories: Ensure .json debug configuration "
+        "files are not deployed to publicly accessible paths on the web server.\n"
+        "Restrict Access: If debug configurations are required in staging environments, "
+        "restrict access to authenticated admin users only via IP whitelisting.\n"
+        "Environment-Specific Builds: Use separate build profiles for development and "
+        "production to ensure debug artifacts are excluded from release bundles.\n"
+        "Audit Build Artifacts: Regularly scan deployment artifacts for unintended "
+        "inclusion of debug or configuration files.\n"
+        "
" + ) + + IMPACT_WITH_PRE = ( + '
\n'
+        "Information Disclosure: Attackers can discover internal microservice endpoints, "
+        "feature flag states, and environment-specific configuration values.\n"
+        "Lateral Movement: Revealed internal addresses may allow attackers to probe "
+        "backend services that are not intended to be publicly reachable.\n"
+        "Credential Exposure: If the debug configuration includes API keys or tokens "
+        "left by developers, this could lead to unauthorized access.\n"
+        "
" + ) + + STEPS_WITH_PRE = ( + '
\n'
+        "Open a web browser.\n"
+        "Navigate to the URL: " + LONG_URL + ".\n"
+        "Observe that the configuration file is accessible and can be downloaded.\n"
+        "Review the file contents for internal service addresses and environment variables.\n"
+        "
" + ) + + # Plain markdown content (no embedded
 tags) with a very long unbroken token
+    DESCRIPTION_LONG_TOKEN = (
+        "A session token was observed in the query string: "
+        "token=" + "x" * 300 + " "
+        "which exceeds normal length and may cause rendering issues in reports."
+    )
+
+    def setUp(self):
+        super().setUp()
+        self.user = User.objects.get(username="admin")
+        self.product_type = Product_Type.objects.create(name="Report Test PT")
+        self.product = Product.objects.create(
+            name="Report Test Product",
+            description="Product for report tests",
+            prod_type=self.product_type,
+        )
+        self.engagement = Engagement.objects.create(
+            name="Report Test Engagement",
+            product=self.product,
+            target_start=now(),
+            target_end=now(),
+        )
+        self.test_type = Test_Type.objects.create(name="Report Test Scan")
+        self.test_obj = Test.objects.create(
+            engagement=self.engagement,
+            test_type=self.test_type,
+            title="Report Rendering Test",
+            target_start=now(),
+            target_end=now(),
+        )
+        self.django_engine = engines["django"]
+
+    def _create_finding(self, **kwargs):
+        defaults = {
+            "title": "Debug Configuration File Exposed",
+            "test": self.test_obj,
+            "severity": "Medium",
+            "description": self.DESCRIPTION_WITH_PRE,
+            "mitigation": self.MITIGATION_WITH_PRE,
+            "impact": self.IMPACT_WITH_PRE,
+            "steps_to_reproduce": self.STEPS_WITH_PRE,
+            "active": True,
+            "verified": True,
+            "reporter": self.user,
+            "numerical_severity": "S2",
+            "date": now().date(),
+        }
+        defaults.update(kwargs)
+        return Finding.objects.create(**defaults)
+
+    def _render_finding_report(self, findings):
+        """Render finding_pdf_report.html with the given findings and return HTML."""
+        template = self.django_engine.get_template("dojo/finding_pdf_report.html")
+        context = {
+            "report_name": "Finding Report",
+            "findings": findings,
+            "include_finding_notes": 0,
+            "include_finding_images": 0,
+            "include_executive_summary": 0,
+            "include_table_of_contents": 0,
+            "include_disclaimer": 0,
+            "disclaimer": "",
+            "user": self.user,
+            "team_name": "Test Team",
+            "title": "Finding Report",
+            "host": "http://localhost:8080",
+            "user_id": self.user.id,
+        }
+        return template.render(context)
+
+    def test_no_nested_pre_tags_in_report(self):
+        """
+        Markdown-rendered fields should not produce nested 
 elements.
+
+        When imported data already contains 
 tags (common with BugCrowd CSV
+        imports), the template wrapper must not add an additional 
 layer.
+        The outer wrapper should be a 
. + """ + finding = self._create_finding() + html = self._render_finding_report(Finding.objects.filter(pk=finding.pk)) + + # The template should wrap markdown-rendered fields in div.report-field, + # not in
 tags. We should not see 
 nesting.
+        self.assertNotIn("
", html)
+        self.assertNotIn("
+        # Find the section containing our long token
+        idx = html.index("x" * 300)
+        # Walk backwards to find the nearest opening tag
+        preceding = html[max(0, idx - 500):idx]
+        self.assertIn("report-field", preceding)
+
+    def test_report_base_css_has_overflow_wrap(self):
+        """The report base template must include overflow-wrap for text wrapping."""
+        template = self.django_engine.get_template("report_base.html")
+        # Render with minimal context to get the CSS
+        html = template.render({"report_name": "Test"})
+
+        self.assertIn("overflow-wrap: break-word", html)
+
+    def test_report_base_css_styles_nested_pre(self):
+        """
+        The report base CSS must style .report-field pre to prevent
+        nested 
 elements from breaking out of margins.
+        """
+        template = self.django_engine.get_template("report_base.html")
+        html = template.render({"report_name": "Test"})
+
+        self.assertIn(".report-field pre", html)
+        self.assertIn("overflow-wrap: break-word", html)
+
+    def test_raw_request_pre_tags_preserved(self):
+        """
+        Raw request/response 
 tags should remain unchanged.
+
+        Only markdown-rendered fields should use div.report-field wrappers.
+        The raw_request class pre tags are for literal request/response data
+        and should stay as 
.
+        """
+        template = self.django_engine.get_template("dojo/finding_pdf_report.html")
+        source = template.template.source
+        self.assertIn('class="raw_request"', source)
+        # raw_request should still be inside 
 tags
+        self.assertIn('
', source)