From 99768262a2b552d9d47d494000a7fa556eb8d1ba Mon Sep 17 00:00:00 2001 From: DefectDojo release bot Date: Mon, 22 Sep 2025 16:01:21 +0000 Subject: [PATCH 01/11] Update versions in application files --- components/package.json | 2 +- helm/defectdojo/Chart.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/package.json b/components/package.json index 5645dac3561..d96e5e51133 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "defectdojo", - "version": "2.50.3", + "version": "2.51.0-dev", "license" : "BSD-3-Clause", "private": true, "dependencies": { diff --git a/helm/defectdojo/Chart.yaml b/helm/defectdojo/Chart.yaml index a41da47c4b6..0d53ef42b73 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 -appVersion: "2.50.3" +appVersion: "2.51.0-dev" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.6.208 +version: 1.6.209-dev icon: https://www.defectdojo.org/img/favicon.ico maintainers: - name: madchap From afcb3e5d65e35967fa255914d61fab34f9c3ebba Mon Sep 17 00:00:00 2001 From: Dmitriy Shafranskiy <167208426+dshafranskiy-r7@users.noreply.github.com> Date: Tue, 23 Sep 2025 18:46:33 +0200 Subject: [PATCH 02/11] make sonarqube hotspots sync work (#13206) * sonarqube hotspots sync implementation * params fix * ruff fixes * ruff fixes --- dojo/tools/api_sonarqube/api_client.py | 65 +++++++++ dojo/tools/api_sonarqube/updater.py | 177 ++++++++++++++++-------- unittests/test_api_sonarqube_updater.py | 121 ++++++++++++---- 3 files changed, 279 insertions(+), 84 deletions(-) diff --git a/dojo/tools/api_sonarqube/api_client.py b/dojo/tools/api_sonarqube/api_client.py index 9c04ceea648..91a7673c812 100644 --- a/dojo/tools/api_sonarqube/api_client.py +++ b/dojo/tools/api_sonarqube/api_client.py @@ -334,6 +334,31 @@ def get_hotspot_rule(self, rule_id): self.rules_cache.update({rule_id: rule}) return rule + def get_hotspot(self, rule_id): + """ + Get detailed information about a hotspot + :param rule_id: + :return: + """ + rule = self.rules_cache.get(rule_id) + if not rule: + response = self.session.get( + url=f"{self.sonar_api_url}/hotspots/search", + params={"hotspots": rule_id}, + headers=self.default_headers, + timeout=settings.REQUESTS_TIMEOUT, + ) + if not response.ok: + msg = ( + f"Unable to get the hotspot rule {rule_id} " + f"due to {response.status_code} - {response.content}" + ) + raise Exception(msg) + + rule = response.json()["hotspots"][0] + self.rules_cache.update({rule_id: rule}) + return rule + def transition_issue(self, issue_key, transition): """ Do workflow transition on an issue. Requires authentication and Browse permission on project. @@ -375,6 +400,46 @@ def transition_issue(self, issue_key, transition): ) raise Exception(msg) + def transition_hotspot(self, issue_key, status, resolution=None): + """ + Do workflow transition on an issue. Requires authentication and Browse permission on project. + The transitions 'wontfix' and 'falsepositive' require the permission 'Administer Issues'. + The transitions involving security hotspots (except 'requestreview') require + the permission 'Administer Security Hotspot'. + + Possible resolution values: + - FIXED + - SAFE + - ACKNOWLEDGED + + Possible status values: + - TO_REVIEW + - REVIEWED + + :param issue_key: + :param status: + :param resolution: + :return: + """ + data = {"hotspot": issue_key, "status": status} + + if resolution: + data["resolution"] = resolution + + response = self.session.post( + url=f"{self.sonar_api_url}/hotspots/change_status", + data=data, + headers=self.default_headers, + timeout=settings.REQUESTS_TIMEOUT, + ) + + if not response.ok: + msg = ( + f"Unable to change status {status} / resolution {resolution} the issue {issue_key} " + f'due to {response.status_code} - {response.content.decode("utf-8")}' + ) + raise Exception(msg) + def add_comment(self, issue_key, text): """ Add a comment. diff --git a/dojo/tools/api_sonarqube/updater.py b/dojo/tools/api_sonarqube/updater.py index c964760526f..b2417dd22b0 100644 --- a/dojo/tools/api_sonarqube/updater.py +++ b/dojo/tools/api_sonarqube/updater.py @@ -52,6 +52,45 @@ class SonarQubeApiUpdater: }, ] + MAPPING_SONARQUBE_HOTSPOT_STATUS_TRANSITION = [ + { + "from": ["TO_REVIEW"], + "to": "RESOLVED / FALSE-POSITIVE", + "transition": "REVIEWED", + "resolution": "SAFE", + }, + { + "from": ["TO_REVIEW"], + "to": "RESOLVED / FIXED", + "transition": "REVIEWED", + "resolution": "FIXED", + }, + { + "from": ["TO_REVIEW"], + "to": "RESOLVED / WONTFIX", + "transition": "REVIEWED", + "resolution": "ACKNOWLEDGED", + }, + { + "from": ["REVIEWED"], + "to": "OPEN", + "transition": "TO_REVIEW", + "resolution": None, + }, + { + "from": ["REVIEWED"], + "to": "REOPENED", + "transition": "TO_REVIEW", + "resolution": None, + }, + { + "from": ["REVIEWED"], + "to": "CONFIRMED", + "transition": "TO_REVIEW", + "resolution": None, + }, + ] + @staticmethod def get_sonarqube_status_for(finding): target_status = None @@ -66,16 +105,22 @@ def get_sonarqube_status_for(finding): return target_status def get_sonarqube_required_transitions_for( - self, current_status, target_status, - ): + self, current_status, target_status, is_hotspot): # If current and target is the same... do nothing if current_status == target_status: return None + # Select the appropriate mapping based on issue type + mapping = ( + self.MAPPING_SONARQUBE_HOTSPOT_STATUS_TRANSITION + if is_hotspot + else self.MAPPING_SONARQUBE_STATUS_TRANSITION + ) + # Check if there is at least one transition from current_status... if not [ x - for x in self.MAPPING_SONARQUBE_STATUS_TRANSITION + for x in mapping if current_status in x.get("from") ]: return None @@ -84,7 +129,7 @@ def get_sonarqube_required_transitions_for( # can transition to target_status transitions = [ x - for x in self.MAPPING_SONARQUBE_STATUS_TRANSITION + for x in mapping if target_status == x.get("to") ] if transitions: @@ -92,26 +137,38 @@ def get_sonarqube_required_transitions_for( # There is a direct transition from current status... if current_status in transition.get("from"): t = transition.get("transition") + if is_hotspot: + return [{"status": t, "resolution": transition.get("resolution")}] if t else None return [t] if t else None - # We have the last transition to get to our target status but there - # is no direct transition - transitions_result = deque() - transitions_result.appendleft(transitions[0].get("transition")) + # Handle complex transitions for regular issues + if not is_hotspot: + # We have the last transition to get to our target status but there + # is no direct transition + transitions_result = deque() + transitions_result.appendleft(transitions[0].get("transition")) - # Find out previous transitions that would finish in any FROM of a - # previous to use as target - for transition in transitions: - for t_from in transition.get("from"): - possible_transition = ( - self.get_sonarqube_required_transitions_for( - current_status, t_from, + # Find out previous transitions that would finish in any FROM of a + # previous to use as target + for transition in transitions: + for t_from in transition.get("from"): + possible_transition = ( + self.get_sonarqube_required_transitions_for( + current_status, t_from, is_hotspot, + ) ) - ) - if possible_transition: - transitions_result.extendleft(possible_transition) - return list(transitions_result) - return None + if possible_transition: + transitions_result.extendleft(possible_transition) + return list(transitions_result) + else: + # SQ code is too complicated for ISSUES, there is no such thing for HOTSPOTS, + # there are only 2 states: TO_REVIEW and REVIEWED + transitions_result = deque() + transitions_result.appendleft( + {"status": transitions[0].get("transition"), + "resolution": transitions[0].get("resolution")}, + ) + return list(transitions_result) return None def update_sonarqube_finding(self, finding): @@ -128,44 +185,56 @@ def update_sonarqube_finding(self, finding): # during import target_status = self.get_sonarqube_status_for(finding) + is_hotspot = sonarqube_issue.type == "SECURITY_HOTSPOT" - issue = client.get_issue(sonarqube_issue.key) - if ( - issue - ): # Issue could have disappeared in SQ because a previous scan has resolved the issue as fixed - if issue.get("resolution"): - current_status = "{} / {}".format( - issue.get("status"), issue.get("resolution"), - ) - else: - current_status = issue.get("status") + issue = client.get_hotspot(sonarqube_issue.key) if is_hotspot else client.get_issue(sonarqube_issue.key) + + # Issue does not exist (could have disappeared in SQ because a previous scan resolved it) + if not issue: + return + + if is_hotspot: + current_status = issue.get("status") + elif issue.get("resolution"): + current_status = "{} / {}".format(issue.get("status"), issue.get("resolution")) + else: + current_status = issue.get("status") + # Get required transitions + transitions = self.get_sonarqube_required_transitions_for( + current_status, target_status, is_hotspot=is_hotspot, + ) + + if not transitions: logger.debug( - f"--> SQ Current status: {current_status}. Current target status: {target_status}", + f"There are no transitions between {current_status} and {target_status} for finding '{finding}' in SonarQube", ) + return - transitions = self.get_sonarqube_required_transitions_for( - current_status, target_status, + logger.debug( + f"Updating finding '{finding}' transition {current_status} -> {target_status} in SonarQube", ) - if transitions: - logger.info( - f"Updating finding '{finding}' in SonarQube", - ) - for transition in transitions: - client.transition_issue(sonarqube_issue.key, transition) - - # Track Defect Dojo has updated the SonarQube issue - Sonarqube_Issue_Transition.objects.create( - sonarqube_issue=finding.sonarqube_issue, - # not sure if this is needed, but looks like the original author decided to send display status - # to sonarqube we changed Accepted into Risk Accepted, but we change it back to be sure we don't - # break the integration - finding_status=finding.status().replace( - "Risk Accepted", "Accepted", - ) - if finding.status() - else finding.status(), - sonarqube_status=current_status, - transitions=",".join(transitions), - ) + # Apply transitions + for transition in transitions: + if is_hotspot: + client.transition_hotspot(sonarqube_issue.key, + status=transition["status"], + resolution=transition["resolution"]) + else: + client.transition_issue(sonarqube_issue.key, transition) + + # Track that Defect Dojo has updated the SonarQube issue + Sonarqube_Issue_Transition.objects.create( + sonarqube_issue=finding.sonarqube_issue, + # not sure if this is needed, but looks like the original author decided to send display status + # to sonarqube we changed Accepted into Risk Accepted, but we change it back to be sure we don't + # break the integration + finding_status=finding.status().replace( + "Risk Accepted", "Accepted", + ) + if finding.status() + else finding.status(), + sonarqube_status=current_status, + transitions=",".join(transition["status"] if is_hotspot else transition for transition in transitions), + ) diff --git a/unittests/test_api_sonarqube_updater.py b/unittests/test_api_sonarqube_updater.py index 52ee6ed8f9f..4fb6377e784 100644 --- a/unittests/test_api_sonarqube_updater.py +++ b/unittests/test_api_sonarqube_updater.py @@ -12,86 +12,147 @@ def setUp(self): self.updater = SonarQubeApiUpdater() - def test_transitions_for_sonarqube_from_open_1(self): + def test_transitions_for_sonarqube_issue_from_open_to_confirmed(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("OPEN", "CONFIRMED"), + self.updater.get_sonarqube_required_transitions_for("OPEN", "CONFIRMED", is_hotspot=False), ["confirm"], ) - def test_transitions_for_sonarqube_from_open_2(self): + def test_transitions_for_sonarqube_issue_from_open_to_resolved_fixed(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("OPEN", "RESOLVED / FIXED"), + self.updater.get_sonarqube_required_transitions_for("OPEN", "RESOLVED / FIXED", is_hotspot=False), ["resolve"], ) - def test_transitions_for_sonarqube_from_reopened_1(self): + def test_transitions_for_sonarqube_issue_from_reopened_to_resolved_fixed(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("REOPENED", "RESOLVED / FIXED"), + self.updater.get_sonarqube_required_transitions_for("REOPENED", "RESOLVED / FIXED", is_hotspot=False), ["resolve"], ) - def test_transitions_for_sonarqube_from_reopened_2(self): + def test_transitions_for_sonarqube_issue_from_reopened_to_confirmed(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("REOPENED", "CONFIRMED"), + self.updater.get_sonarqube_required_transitions_for("REOPENED", "CONFIRMED", is_hotspot=False), ["confirm"], ) - def test_transitions_for_sonarqube_from_resolved_1(self): + def test_transitions_for_sonarqube_issue_from_resolved_fixed_to_confirmed(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("RESOLVED / FIXED", "CONFIRMED"), + self.updater.get_sonarqube_required_transitions_for("RESOLVED / FIXED", "CONFIRMED", is_hotspot=False), ["reopen", "confirm"], ) - def test_transitions_for_sonarqube_from_resolved_2(self): + def test_transitions_for_sonarqube_issue_from_resolved_fixed_to_resolved_falsepositive(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("RESOLVED / FIXED", "RESOLVED / FALSE-POSITIVE"), + self.updater.get_sonarqube_required_transitions_for("RESOLVED / FIXED", "RESOLVED / FALSE-POSITIVE", is_hotspot=False), ["reopen", "falsepositive"], ) - def test_transitions_for_sonarqube_from_resolved_3(self): + def test_transitions_for_sonarqube_issue_from_resolved_fixed_to_resolved_wontfix(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("RESOLVED / FIXED", "RESOLVED / WONTFIX"), + self.updater.get_sonarqube_required_transitions_for("RESOLVED / FIXED", "RESOLVED / WONTFIX", is_hotspot=False), ["reopen", "wontfix"], ) - def test_transitions_for_sonarqube_fake_target_origin(self): + def test_transitions_for_sonarqube_issue_from_confirmed_to_reopened(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("FAKE_STATUS", "RESOLVED / FIXED"), + self.updater.get_sonarqube_required_transitions_for("CONFIRMED", "REOPENED", is_hotspot=False), + ["unconfirm"], + ) + + def test_transitions_for_sonarqube_issue_from_confirmed_to_resolved_fixed(self): + self.assertEqual( + self.updater.get_sonarqube_required_transitions_for("CONFIRMED", "RESOLVED / FIXED", is_hotspot=False), + ["resolve"], + ) + + def test_transitions_for_sonarqube_issue_from_confirmed_to_resolved_wontfix(self): + self.assertEqual( + self.updater.get_sonarqube_required_transitions_for("CONFIRMED", "RESOLVED / WONTFIX", is_hotspot=False), + ["wontfix"], + ) + + def test_transitions_for_sonarqube_issue_from_confirmed_to_resolved_falsepositive(self): + self.assertEqual( + self.updater.get_sonarqube_required_transitions_for("CONFIRMED", "RESOLVED / FALSE-POSITIVE", is_hotspot=False), + ["falsepositive"], + ) + + def test_transitions_for_sonarqube_issue_open_reopen_status_same(self): + self.assertEqual( + self.updater.get_sonarqube_required_transitions_for("OPEN", "OPEN", is_hotspot=False), None, ) - def test_transitions_for_sonarqube_fake_target_status(self): + def test_transitions_for_sonarqube_issue_open_reopen_status_different(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("RESOLVED / FIXED", "FAKE_STATUS"), + self.updater.get_sonarqube_required_transitions_for("OPEN", "REOPENED", is_hotspot=False), None, ) - def test_transitions_for_sonarqube_from_confirmed_1(self): + def test_transitions_for_sonarqube_issue_fake_status(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("CONFIRMED", "REOPENED"), - ["unconfirm"], + self.updater.get_sonarqube_required_transitions_for("FAKE_STATUS", "RESOLVED / FIXED", is_hotspot=False), + None, ) - def test_transitions_for_sonarqube_from_confirmed_2(self): + def test_transitions_for_sonarqube_issue_fake_target(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("CONFIRMED", "RESOLVED / FIXED"), - ["resolve"], + self.updater.get_sonarqube_required_transitions_for("RESOLVED / FIXED", "FAKE_STATUS", is_hotspot=False), + None, + ) + + # Tests for hotspot transitions + def test_transitions_for_sonarqube_hotspot_from_to_review_to_resolved_falsepositive(self): + self.assertEqual( + self.updater.get_sonarqube_required_transitions_for("TO_REVIEW", "RESOLVED / FALSE-POSITIVE", is_hotspot=True), + [{"status": "REVIEWED", "resolution": "SAFE"}], + ) + + def test_transitions_for_sonarqube_hotspot_from_to_review_to_resolved_fixed(self): + self.assertEqual( + self.updater.get_sonarqube_required_transitions_for("TO_REVIEW", "RESOLVED / FIXED", is_hotspot=True), + [{"status": "REVIEWED", "resolution": "FIXED"}], + ) + + def test_transitions_for_sonarqube_hotspot_from_to_review_to_resolved_wontfix(self): + self.assertEqual( + self.updater.get_sonarqube_required_transitions_for("TO_REVIEW", "RESOLVED / WONTFIX", is_hotspot=True), + [{"status": "REVIEWED", "resolution": "ACKNOWLEDGED"}], + ) + + def test_transitions_for_sonarqube_hotspot_from_reviewed_to_open(self): + self.assertEqual( + self.updater.get_sonarqube_required_transitions_for("REVIEWED", "OPEN", is_hotspot=True), + [{"status": "TO_REVIEW", "resolution": None}], + ) + + def test_transitions_for_sonarqube_hotspot_from_reviewed_to_reopened(self): + self.assertEqual( + self.updater.get_sonarqube_required_transitions_for("REVIEWED", "REOPENED", is_hotspot=True), + [{"status": "TO_REVIEW", "resolution": None}], + ) + + def test_transitions_for_sonarqube_hotspot_from_reviewed_to_confirmed(self): + self.assertEqual( + self.updater.get_sonarqube_required_transitions_for("REVIEWED", "CONFIRMED", is_hotspot=True), + [{"status": "TO_REVIEW", "resolution": None}], ) - def test_transitions_for_open_reopen_status_1(self): + def test_transitions_for_sonarqube_hotspot_same_status(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("OPEN", "REOPENED"), + self.updater.get_sonarqube_required_transitions_for("TO_REVIEW", "TO_REVIEW", is_hotspot=True), None, ) - def test_transitions_for_open_reopen_status_2(self): + def test_transitions_for_sonarqube_hotspot_fake_status(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("REOPENED", "OPEN"), + self.updater.get_sonarqube_required_transitions_for("FAKE_STATUS", "REVIEWED", is_hotspot=True), None, ) - def test_transitions_for_open_reopen_status_3(self): + def test_transitions_for_sonarqube_hotspot_fake_target(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("REOPENED", "REOPENED"), + self.updater.get_sonarqube_required_transitions_for("TO_REVIEW", "FAKE_STATUS", is_hotspot=True), None, ) From 2fc8befad17f49812faaee64e3008f7ade7f50c6 Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Tue, 23 Sep 2025 18:47:55 +0200 Subject: [PATCH 03/11] :tada: Add fix_available information to blackduck binary analysis parser #12633 (#13236) --- dojo/tools/blackduck_binary_analysis/parser.py | 6 ++++-- unittests/tools/test_blackduck_binary_analysis_parser.py | 7 +------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/dojo/tools/blackduck_binary_analysis/parser.py b/dojo/tools/blackduck_binary_analysis/parser.py index b0ccd0b9642..6077e016e5e 100644 --- a/dojo/tools/blackduck_binary_analysis/parser.py +++ b/dojo/tools/blackduck_binary_analysis/parser.py @@ -96,12 +96,14 @@ def ingest_findings(self, sorted_findings, test): component_version=i.version, unique_id_from_tool=unique_finding_key, ) - if cvss_v3: finding.cvssv3 = cvss_vectors else: finding.severity_justification = cvss_vectors - + if i.latest_version: + finding.fix_available = True + else: + finding.fix_available = False findings[unique_finding_key] = finding return list(findings.values()) diff --git a/unittests/tools/test_blackduck_binary_analysis_parser.py b/unittests/tools/test_blackduck_binary_analysis_parser.py index d378de0567d..4d74c8914b7 100644 --- a/unittests/tools/test_blackduck_binary_analysis_parser.py +++ b/unittests/tools/test_blackduck_binary_analysis_parser.py @@ -22,26 +22,21 @@ def test_parse_one_vuln(self): "instrument.dll: zlib 1.2.13 Vulnerable to CVE-2023-45853", finding.title, ) - + self.assertEqual(True, finding.fix_available) self.assertIsNotNone(finding.description) self.assertIsNotNone(finding.severity) self.assertEqual("Critical", finding.severity) - self.assertIsNotNone(finding.component_name) self.assertEqual("zlib", finding.component_name) - self.assertIsNotNone(finding.component_version) self.assertEqual("1.2.13", finding.component_version) - self.assertIsNotNone(finding.file_path) self.assertEqual( "JRE.msi:JRE.msi-30276-90876123.cab:instrument.dll", finding.file_path, ) - self.assertIsNotNone(finding.vuln_id_from_tool) self.assertEqual("CVE-2023-45853", finding.vuln_id_from_tool) - self.assertIsNotNone(finding.unique_id_from_tool) def test_parse_many_vulns(self): From 7c0f3f0252c7a76d5c5863ca186360c7c3856d2d Mon Sep 17 00:00:00 2001 From: Jino Tesauro Date: Wed, 24 Sep 2025 11:40:25 -0500 Subject: [PATCH 04/11] Added line number field for SonarQube Scan and SonarQube Scan Detailed scantype --- dojo/tools/sonarqube/sonarqube_restapi_json.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dojo/tools/sonarqube/sonarqube_restapi_json.py b/dojo/tools/sonarqube/sonarqube_restapi_json.py index 205a5e14b1b..c1e019cf489 100644 --- a/dojo/tools/sonarqube/sonarqube_restapi_json.py +++ b/dojo/tools/sonarqube/sonarqube_restapi_json.py @@ -49,6 +49,7 @@ def get_json_items(self, json_content, test, mode): static_finding=True, dynamic_finding=False, tags=["bug"], + line=line, ) elif issue.get("type") == "VULNERABILITY": key = issue.get("key") @@ -58,6 +59,7 @@ def get_json_items(self, json_content, test, mode): flows = issue.get("flows", []) status = issue.get("status") message = issue.get("message") + line = str(issue.get("line")) cwe = None if "Category: CWE-" in message: cwe_pattern = r"Category: CWE-\d{1,5}" @@ -91,6 +93,7 @@ def get_json_items(self, json_content, test, mode): description += "**rule:** " + rule + "\n" description += "**component:** " + component + "\n" description += "**project:** " + project + "\n" + description += "**line:** " + line + "\n" if flows != []: description += "**flows:** " + str(flows) + "\n" description += "**status:** " + status + "\n" @@ -115,6 +118,7 @@ def get_json_items(self, json_content, test, mode): cvssv3_score=cvss, file_path=component, tags=["vulnerability"], + line=line, ) vulnids = [] if "Reference: CVE" in message: @@ -180,6 +184,7 @@ def get_json_items(self, json_content, test, mode): dynamic_finding=False, file_path=component, tags=["code_smell"], + line=line, ) items.append(item) if json_content.get("hotspots"): @@ -223,6 +228,7 @@ def get_json_items(self, json_content, test, mode): dynamic_finding=False, file_path=component, tags=["hotspot"], + line=line, ) items.append(item) return items From 9bf7a326bd55f56a863b2ca767892ab1617d6d7c Mon Sep 17 00:00:00 2001 From: Filipe Pina <636320+fopina@users.noreply.github.com> Date: Fri, 26 Sep 2025 18:05:22 +0100 Subject: [PATCH 05/11] Revert "Bump django-tagulous from 2.1.0 to 2.1.1" and "Tags: Remove duplicates in edit forms" and bump django-multiselectfield from 0.1.13 to 1.0.1 (#13250) This reverts commits 008cdffd01f9c4ddae0dca78d7e51eb9986473e4 and 887d426c64a387867f5b33fcb8ef5bcd5f2c64ee. Also cherryicks b5ef5548246969777452abfedef8996e5265e0c0 from #12601 --- dojo/forms.py | 21 --------------------- requirements.txt | 5 +++-- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/dojo/forms.py b/dojo/forms.py index 2a912bd678c..eae48e4e937 100644 --- a/dojo/forms.py +++ b/dojo/forms.py @@ -342,10 +342,6 @@ class ProductForm(forms.ModelForm): product_manager = forms.ModelChoiceField(queryset=Dojo_User.objects.exclude(is_active=False).order_by("first_name", "last_name"), required=False) technical_contact = forms.ModelChoiceField(queryset=Dojo_User.objects.exclude(is_active=False).order_by("first_name", "last_name"), required=False) team_manager = forms.ModelChoiceField(queryset=Dojo_User.objects.exclude(is_active=False).order_by("first_name", "last_name"), required=False) - tags = TagField( - required=False, - help_text="Add tags that help describe this product. Choose from the list or add new tags. Press Enter key to add.", - ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -1017,10 +1013,6 @@ class EngForm(forms.ModelForm): queryset=None, required=True, label="Testing Lead") test_strategy = forms.URLField(required=False, label="Test Strategy URL") - tags = TagField( - required=False, - help_text="Add tags that help describe this engagement. Choose from the list or add new tags. Press Enter key to add.", - ) def __init__(self, *args, **kwargs): cicd = False @@ -1103,10 +1095,6 @@ class TestForm(forms.ModelForm): lead = forms.ModelChoiceField( queryset=None, required=False, label="Testing Lead") - tags = TagField( - required=False, - help_text="Add tags that help describe this test. Choose from the list or add new tags. Press Enter key to add.", - ) def __init__(self, *args, **kwargs): obj = None @@ -1463,10 +1451,6 @@ class FindingForm(forms.ModelForm): choices=EFFORT_FOR_FIXING_CHOICES, error_messages={ "invalid_choice": EFFORT_FOR_FIXING_INVALID_CHOICE}) - tags = TagField( - required=False, - help_text="Add tags that help describe this finding. Choose from the list or add new tags. Press Enter key to add.", - ) # the only reliable way without hacking internal fields to get predicatble ordering is to make it explicit field_order = ("title", "group", "date", "sla_start_date", "sla_expiration_date", "cwe", "vulnerability_ids", "severity", "cvss_info", "cvssv3", @@ -1738,11 +1722,6 @@ class Meta: class EditEndpointForm(forms.ModelForm): - tags = TagField( - required=False, - help_text="Add tags that help describe this endpoint. Choose from the list or add new tags. Press Enter key to add.", - ) - class Meta: model = Endpoint exclude = ["product", "inherited_tags"] diff --git a/requirements.txt b/requirements.txt index dc0f1f2f8a2..40b83c9fd8f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ django-dbbackup==4.3.0 django-environ==0.12.0 django-filter==25.1 django-imagekit==5.0.0 -django-multiselectfield==0.1.13 +django-multiselectfield==1.0.1 django-polymorphic==4.1.0 django-crispy-forms==2.4 django_extensions==4.1 @@ -55,7 +55,8 @@ django-debug-toolbar==5.2.0 django-debug-toolbar-request-history==0.1.4 vcrpy==7.0.0 vcrpy-unittest==0.1.7 -django-tagulous==2.1.1 +# do not upgrade to 2.1.1 - https://github.com/DefectDojo/django-DefectDojo/issues/12918 +django-tagulous==2.1.0 PyJWT==2.10.1 cvss==3.6 django-fieldsignals==0.7.0 From 043aee40ca681f5834ee83ae69ad98de220936cb Mon Sep 17 00:00:00 2001 From: Paul Osinski Date: Fri, 26 Sep 2025 14:58:39 -0400 Subject: [PATCH 06/11] update changelog 2.50.3 --- docs/content/en/changelog/changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/content/en/changelog/changelog.md b/docs/content/en/changelog/changelog.md index 2cc71a0a6f7..f45d0a04005 100644 --- a/docs/content/en/changelog/changelog.md +++ b/docs/content/en/changelog/changelog.md @@ -10,6 +10,10 @@ For Open Source release notes, please see the [Releases page on GitHub](https:// ## Sept 2025: v2.50 +### Sept 15, 2025: v2.50.3 + +* **(Pro UI)** Added support for [CVSSv4.0](https://www.first.org/cvss/v4-0/) vector strings. + ### Sept 15, 2025: v2.50.2 * **(Pro UI)** Added Any/All status filtering. Filtering by status allows you to apply either AND (inner join) logic, or OR (outer join) logic to the filter. From 0ef0eb2af0cab08f49c4c2cb7184787441b4764a Mon Sep 17 00:00:00 2001 From: Jino Tesauro Date: Fri, 26 Sep 2025 14:19:51 -0500 Subject: [PATCH 07/11] Added branch_tag to jira epic description --- dojo/jira_link/helper.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/dojo/jira_link/helper.py b/dojo/jira_link/helper.py index 87dab3d291b..46e052c7d17 100644 --- a/dojo/jira_link/helper.py +++ b/dojo/jira_link/helper.py @@ -1405,10 +1405,15 @@ def update_epic(engagement, **kwargs): epic_name = kwargs.get("epic_name") if not epic_name: epic_name = engagement.name + + description = epic_name + branch_tag = engagement.branch_tag + if branch_tag: + description += "\nBranch: " + branch_tag jira_issue_update_kwargs = { "summary": epic_name, - "description": epic_name, + "description": description, } if (epic_priority := kwargs.get("epic_priority")) is not None: jira_issue_update_kwargs["priority"] = {"name": epic_priority} @@ -1443,12 +1448,16 @@ def add_epic(engagement, **kwargs): epic_issue_type_name = getattr(jira_project, "epic_issue_type_name", "Epic") if not epic_name: epic_name = engagement.name + description = epic_name + branch_tag = engagement.branch_tag + if branch_tag: + description += "\nBranch: " + branch_tag issue_dict = { "project": { "key": jira_project.project_key, }, "summary": epic_name, - "description": epic_name, + "description": description, "issuetype": { "name": epic_issue_type_name, }, From 7bc183537966b2cde2a6e5b74105c92123005922 Mon Sep 17 00:00:00 2001 From: Jino Tesauro Date: Fri, 26 Sep 2025 14:43:49 -0500 Subject: [PATCH 08/11] Fixing ruff linter issue --- dojo/jira_link/helper.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/dojo/jira_link/helper.py b/dojo/jira_link/helper.py index 46e052c7d17..a28ca353301 100644 --- a/dojo/jira_link/helper.py +++ b/dojo/jira_link/helper.py @@ -1401,16 +1401,13 @@ def update_epic(engagement, **kwargs): jira = get_jira_connection(jira_instance) j_issue = get_jira_issue(engagement) issue = jira.issue(j_issue.jira_id) - epic_name = kwargs.get("epic_name") if not epic_name: epic_name = engagement.name - description = epic_name branch_tag = engagement.branch_tag if branch_tag: description += "\nBranch: " + branch_tag - jira_issue_update_kwargs = { "summary": epic_name, "description": description, From 80ec10dd8155ca3392f33f385cd9da90592309ac Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Fri, 26 Sep 2025 22:02:59 +0200 Subject: [PATCH 09/11] product grade logging fix (#13268) --- dojo/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dojo/utils.py b/dojo/utils.py index b11176a10ab..a4f0e67c26a 100644 --- a/dojo/utils.py +++ b/dojo/utils.py @@ -1604,7 +1604,8 @@ def calculate_grade(product, *args, **kwargs): product.prod_numeric_grade = prod_numeric_grade super(Product, product).save() else: - logger.debug("Product %s grade %i is up to date", product.id, prod_numeric_grade) + # Use %s to safely handle None grades without formatter errors + logger.debug("Product %s grade %s is up to date", product.id, prod_numeric_grade) def get_celery_worker_status(): From 0638fdd99f7b90e7186ffc15bd91ef17cd44d68f Mon Sep 17 00:00:00 2001 From: Jino Tesauro Date: Fri, 26 Sep 2025 16:16:47 -0500 Subject: [PATCH 10/11] Added protection for type mismatch --- dojo/tools/sonarqube/sonarqube_restapi_json.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dojo/tools/sonarqube/sonarqube_restapi_json.py b/dojo/tools/sonarqube/sonarqube_restapi_json.py index c1e019cf489..f56f362209c 100644 --- a/dojo/tools/sonarqube/sonarqube_restapi_json.py +++ b/dojo/tools/sonarqube/sonarqube_restapi_json.py @@ -13,7 +13,7 @@ def get_json_items(self, json_content, test, mode): rule = issue.get("rule") component = issue.get("component") project = issue.get("project") - line = str(issue.get("line")) + line = issue.get("line") textRange = issue.get("textRange", {}) flows = issue.get("flows", []) status = issue.get("status") @@ -28,7 +28,7 @@ def get_json_items(self, json_content, test, mode): description += "**rule:** " + rule + "\n" description += "**component:** " + component + "\n" description += "**project:** " + project + "\n" - description += "**line:** " + line + "\n" + description += "**line:** " + str(line) + "\n" if bool(textRange): res = [item + ": " + str(textRange[item]) for item in textRange] description += "**textRange:** " + ", ".join(res) + "\n" @@ -59,7 +59,7 @@ def get_json_items(self, json_content, test, mode): flows = issue.get("flows", []) status = issue.get("status") message = issue.get("message") - line = str(issue.get("line")) + line = issue.get("line") cwe = None if "Category: CWE-" in message: cwe_pattern = r"Category: CWE-\d{1,5}" @@ -93,7 +93,7 @@ def get_json_items(self, json_content, test, mode): description += "**rule:** " + rule + "\n" description += "**component:** " + component + "\n" description += "**project:** " + project + "\n" - description += "**line:** " + line + "\n" + description += "**line:** " + str(line) + "\n" if flows != []: description += "**flows:** " + str(flows) + "\n" description += "**status:** " + status + "\n" @@ -145,7 +145,7 @@ def get_json_items(self, json_content, test, mode): rule = issue.get("rule") component = issue.get("component") project = issue.get("project") - line = str(issue.get("line")) + line = issue.get("line") textRange = issue.get("textRange", {}) flows = issue.get("flows", []) status = issue.get("status") @@ -158,7 +158,7 @@ def get_json_items(self, json_content, test, mode): description += "**rule:** " + rule + "\n" description += "**component:** " + component + "\n" description += "**project:** " + project + "\n" - description += "**line:** " + line + "\n" + description += "**line:** " + str(line) + "\n" if bool(textRange): res = [] for item in textRange: @@ -194,7 +194,7 @@ def get_json_items(self, json_content, test, mode): project = hotspot.get("project") securityCategory = hotspot.get("securityCategory") status = hotspot.get("status") - line = str(hotspot.get("line")) + line = hotspot.get("line") message = hotspot.get("message") textRange = hotspot.get("textRange", {}) flows = hotspot.get("flows", []) @@ -206,7 +206,7 @@ def get_json_items(self, json_content, test, mode): description += "**project:** " + project + "\n" description += "**securityCategory:** " + securityCategory + "\n" description += "**status:** " + status + "\n" - description += "**line:** " + line + "\n" + description += "**line:** " + str(line) + "\n" description += "**message:** " + message + "\n" if bool(textRange): res = [] From 3480313b319c6151b7550737425eb9e551e96384 Mon Sep 17 00:00:00 2001 From: DefectDojo release bot Date: Mon, 29 Sep 2025 13:23:30 +0000 Subject: [PATCH 11/11] Update versions in application files --- components/package.json | 2 +- dojo/__init__.py | 2 +- helm/defectdojo/Chart.yaml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/package.json b/components/package.json index d96e5e51133..91f406d22f5 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "defectdojo", - "version": "2.51.0-dev", + "version": "2.50.4", "license" : "BSD-3-Clause", "private": true, "dependencies": { diff --git a/dojo/__init__.py b/dojo/__init__.py index 9430de8cf28..5737b3ff975 100644 --- a/dojo/__init__.py +++ b/dojo/__init__.py @@ -4,6 +4,6 @@ # Django starts so that shared_task will use this app. from .celery import app as celery_app # noqa: F401 -__version__ = "2.50.3" +__version__ = "2.50.4" __url__ = "https://github.com/DefectDojo/django-DefectDojo" __docs__ = "https://documentation.defectdojo.com" diff --git a/helm/defectdojo/Chart.yaml b/helm/defectdojo/Chart.yaml index 0d53ef42b73..8620c885e19 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 -appVersion: "2.51.0-dev" +appVersion: "2.50.4" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.6.209-dev +version: 1.6.209 icon: https://www.defectdojo.org/img/favicon.ico maintainers: - name: madchap