Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "defectdojo",
"version": "3.1.0",
"version": "3.2.0-dev",
"license": "BSD-3-Clause",
"private": true,
"dependencies": {
Expand Down
7 changes: 7 additions & 0 deletions docs/content/en/open_source/upgrading/3.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: 'Upgrading to DefectDojo Version 3.2.x'
toc_hide: true
weight: -20260706
description: No special instructions.
---
There are no special instructions for upgrading to 3.2.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/3.2.0) for the contents of the release.
2 changes: 1 addition & 1 deletion dojo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
# Django starts so that shared_task will use this app.
from .celery import app as celery_app # noqa: F401

__version__ = "3.1.0"
__version__ = "3.2.0-dev"
__url__ = "https://github.com/DefectDojo/django-DefectDojo"
__docs__ = "https://documentation.defectdojo.com"
51 changes: 43 additions & 8 deletions dojo/tools/api_sonarqube/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,22 @@ def import_issues(self, test):
sonarqube_permalink = "No permalink \n"

rule_details = self.get_rule_details(rule)
mitigation = "No mitigation provided" # Default fallback

if rule_details:
description = self.clean_rule_description_html(
rule_details,
)
cwe = self.clean_cwe(rule_details)
references = sonarqube_permalink + self.get_references(rule_details)

# Dynamic extraction for modern descriptionSections payloads
if "descriptionSections" in rule:
for section in rule.get("descriptionSections", []):
if isinstance(section, dict) and section.get("key") == "how_to_fix":
content = section.get("content", "")
if content:
mitigation = self.sanitize_rule_details(content)
else:
description = ""
cwe = None
Expand Down Expand Up @@ -234,7 +244,7 @@ def import_issues(self, test):
duplicate=False,
out_of_scope=False,
mitigated=None,
mitigation="No mitigation provided",
mitigation=mitigation, # Dynamic assignment
impact="No impact provided",
static_finding=True,
sonarqube_issue=sonarqube_issue,
Expand Down Expand Up @@ -400,15 +410,40 @@ def clean_cwe(raw_html):

@staticmethod
def get_rule_details(rule):
# Path 1: Legacy HTML Support
if html_desc := rule.get("htmlDesc"):
return SonarQubeApiImporter.sanitize_rule_details(html_desc)
if not (md_desc := rule.get("mdDesc")):
return ""
# SonarQube 2025.x can return markdown-only rule descriptions, including
# inline HTML that should still be treated as markdown content.
return SonarQubeApiImporter.sanitize_rule_details(
markdown.markdown(md_desc, extensions=["extra"]),
)

# Path 2: Legacy Markdown Support
if md_desc := rule.get("mdDesc"):
return SonarQubeApiImporter.sanitize_rule_details(
markdown.markdown(md_desc, extensions=["extra"]),
)

# Path 3: Modern/Conditional Structured Sections Support (SonarQube 2025/2026+)
if "descriptionSections" in rule:
sections = rule["descriptionSections"]
if isinstance(sections, list):
sections_map = {}
for section in sections:
if isinstance(section, dict) and "key" in section and "content" in section:
sections_map[section["key"]] = section["content"]

payload_elements = []
if root_cause := sections_map.get("root_cause"):
payload_elements.append(f"<h3>Root Cause</h3>{root_cause}")
if how_to_fix := sections_map.get("how_to_fix"):
# Folded into description text; main mitigation field remains default
payload_elements.append(f"<h3>Mitigation</h3>{how_to_fix}")
if resources := sections_map.get("resources"):
# Must use h2 to correctly trigger clean_rule_description_html() truncation
payload_elements.append(f"<h2>References</h2>{resources}")

if payload_elements:
combined_payload = "".join(payload_elements)
return SonarQubeApiImporter.sanitize_rule_details(combined_payload)

return ""

@staticmethod
def sanitize_rule_details(description):
Expand Down
8 changes: 4 additions & 4 deletions helm/defectdojo/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
apiVersion: v2
appVersion: "3.1.0"
appVersion: "3.2.0-dev"
description: A Helm chart for Kubernetes to install DefectDojo
name: defectdojo
version: 1.9.35
version: 1.9.36-dev
icon: https://defectdojo.com/hubfs/DefectDojo_favicon.png
maintainers:
- name: madchap
Expand Down Expand Up @@ -33,5 +33,5 @@ dependencies:
# - kind: security
# description: Critical bug
annotations:
artifacthub.io/prerelease: "false"
artifacthub.io/changes: "- kind: changed\n description: chore(deps)_ update valkey _ tag from 0.20.2 to v0.22.1 (_/defect_/chart.yaml)\n- kind: changed\n description: Update gcr.io/cloudsql__/gce_proxy Docker tag from 1.38.0 to v1.38.1 (_/defect_/values.yaml)\n- kind: changed\n description: Bump DefectDojo to 3.1.0\n"
artifacthub.io/prerelease: "true"
artifacthub.io/changes: ""
2 changes: 1 addition & 1 deletion helm/defectdojo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ The HELM schema will be generated for you.

# General information about chart values

![Version: 1.9.35](https://img.shields.io/badge/Version-1.9.35-informational?style=flat-square) ![AppVersion: 3.1.0](https://img.shields.io/badge/AppVersion-3.1.0-informational?style=flat-square)
![Version: 1.9.36-dev](https://img.shields.io/badge/Version-1.9.36--dev-informational?style=flat-square) ![AppVersion: 3.2.0-dev](https://img.shields.io/badge/AppVersion-3.2.0--dev-informational?style=flat-square)

A Helm chart for Kubernetes to install DefectDojo

Expand Down
29 changes: 29 additions & 0 deletions unittests/scans/api_sonarqube/issues_single.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[
{
"key": "AYrPQw9oZpu0CyehMfb1",
"rule": "python:S112",
"severity": "MAJOR",
"component": "internal.dummy.project:src/main/python/app.py",
"project": "internal.dummy.project",
"line": 42,
"hash": "abc123def456abc123def456abc123de",
"textRange": {
"startLine": 42,
"endLine": 42,
"startOffset": 4,
"endOffset": 28
},
"flows": [],
"status": "OPEN",
"message": "Replace this generic exception class with a more specific one.",
"effort": "5min",
"debt": "5min",
"author": "developer@example.com",
"tags": [],
"creationDate": "2026-01-01T00:00:00+0000",
"updateDate": "2026-01-01T00:00:00+0000",
"type": "BUG",
"organization": "default-organization",
"fromHotspot": false
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"key": "python:S112",
"repo": "python",
"name": "General exceptions should not be raised",
"createdAt": "2026-01-01T00:00:00Z",
"descriptionSections": [
{
"key": "root_cause",
"content": "<p>Raising generic exceptions reduces code readability.</p>"
},
{
"key": "how_to_fix",
"content": "<p>Raise specific built-in exceptions instead.</p>"
},
{
"key": "resources",
"content": "<p>See OWASP Top 10 or check CWE-95 for details.</p>"
}
]
}
54 changes: 54 additions & 0 deletions unittests/tools/test_api_sonarqube_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ def empty_list(self, *args, **kwargs):
return []


def dummy_rule_description_sections_only(self, *args, **kwargs):
with (get_unit_tests_scans_path("api_sonarqube") / "rule_description_sections_only.json").open(encoding="utf-8") as json_file:
return json.load(json_file)


def dummy_issues_single(self, *args, **kwargs):
with (get_unit_tests_scans_path("api_sonarqube") / "issues_single.json").open(encoding="utf-8") as json_file:
return json.load(json_file)


class TestSonarqubeImporterNoSQToolConfig(DojoTestCase):
# Testing case no 1. https://github.com/DefectDojo/django-DefectDojo/pull/4676
fixtures = [
Expand Down Expand Up @@ -377,6 +387,50 @@ def test_get_rule_details_sanitizes_html_desc(self):
self.assertNotIn("javascript:", rule_details)


class TestSonarqubeImporterDescriptionSections(DojoTestCase):

"""
Verify that the modern descriptionSections API path (SonarQube 2025/2026+)
correctly populates Finding.description and Finding.mitigation.
"""

fixtures = [
"unit_sonarqube_toolType.json",
"unit_sonarqube_toolConfig1.json",
"unit_sonarqube_toolConfig2.json",
"unit_sonarqube_product.json",
"unit_sonarqube_sqcWithKey.json",
]

def setUp(self):
product = Product.objects.get(name="product")
engagement = Engagement(product=product)
self.test = Test(engagement=engagement)

@mock.patch("dojo.tools.api_sonarqube.api_client.SonarQubeAPI.get_project", dummy_product)
@mock.patch("dojo.tools.api_sonarqube.api_client.SonarQubeAPI.get_rule", dummy_rule_description_sections_only)
@mock.patch("dojo.tools.api_sonarqube.api_client.SonarQubeAPI.find_issues", dummy_issues_single)
@mock.patch("dojo.tools.api_sonarqube.api_client.SonarQubeAPI.get_hotspot_rule", dummy_hotspot_rule)
@mock.patch("dojo.tools.api_sonarqube.api_client.SonarQubeAPI.find_hotspots", empty_list)
def test_description_sections_populate_finding_fields(self):
parser = SonarQubeApiImporter()
findings = parser.get_findings(None, self.test)

self.assertEqual(1, len(findings))
finding = findings[0]

# description should contain the root_cause prose but NOT the references section
# (clean_rule_description_html truncates at <h2>References</h2>)
self.assertIn("Raising generic exceptions reduces code readability", finding.description)
self.assertNotIn("OWASP Top 10", finding.description)

# mitigation should be populated from the how_to_fix descriptionSection
self.assertIn("Raise specific built-in exceptions instead", finding.mitigation)

# CWE-95 appears in the resources section and should be extracted
self.assertEqual(95, finding.cwe)


class TestSonarqubeImporterTwoIssuesNoHotspots(DojoTestCase):
# Testing case no 9. https://github.com/DefectDojo/django-DefectDojo/pull/4107
fixtures = [
Expand Down
Loading