From a8cbd10abd889d6a157dae521dedf19fc56e9e08 Mon Sep 17 00:00:00 2001 From: Tejas Saubhage Date: Sun, 15 Mar 2026 00:08:06 -0400 Subject: [PATCH 1/4] Fix Qualys parser collapsing findings with same QID but different ports fixes #13682 --- dojo/tools/qualys/parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dojo/tools/qualys/parser.py b/dojo/tools/qualys/parser.py index d1c5f7c1dd4..35853520116 100644 --- a/dojo/tools/qualys/parser.py +++ b/dojo/tools/qualys/parser.py @@ -320,7 +320,7 @@ def parse_finding(host, tree): if temp_cve_details: refs = temp.get("links", "") finding = Finding( - title="QID-" + gid[4:] + " | " + temp["vuln_name"], + title="QID-" + gid[4:] + " | " + temp["vuln_name"] + (" | Port: " + str(temp["port_status"]) if temp.get("port_status") else ""), mitigation=temp["solution"], description=temp["vuln_description"], severity=sev, @@ -332,7 +332,7 @@ def parse_finding(host, tree): else: finding = Finding( - title="QID-" + gid[4:] + " | " + temp["vuln_name"], + title="QID-" + gid[4:] + " | " + temp["vuln_name"] + (" | Port: " + str(temp["port_status"]) if temp.get("port_status") else ""), mitigation=temp["solution"], description=temp["vuln_description"], severity=sev, From e9e00bf8499ecfc45b54d4b5ae651da35d2fb0cd Mon Sep 17 00:00:00 2001 From: Tejas Saubhage Date: Sun, 15 Mar 2026 22:57:33 -0400 Subject: [PATCH 2/4] Fix Qualys parser: add port to endpoint for per-port finding separation --- dojo/tools/qualys/parser.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dojo/tools/qualys/parser.py b/dojo/tools/qualys/parser.py index 35853520116..7dedf32a4fc 100644 --- a/dojo/tools/qualys/parser.py +++ b/dojo/tools/qualys/parser.py @@ -354,12 +354,14 @@ def parse_finding(host, tree): finding.cvssv3_score = temp.get("CVSS_value") finding.verified = True # manage endpoint/location + host = issue_row["fqdn"] or issue_row["ip_address"] + port = temp.get("port_status") if settings.V3_FEATURE_LOCATIONS: - location = LocationData.url(host=issue_row["fqdn"]) if issue_row["fqdn"] else LocationData.url(host=issue_row["ip_address"]) + location = LocationData.url(host=host, port=int(port) if port else None) finding.unsaved_locations = [location] else: # TODO: Delete this after the move to Locations - location = Endpoint(host=issue_row["fqdn"]) if issue_row["fqdn"] else Endpoint(host=issue_row["ip_address"]) + location = Endpoint(host=host, port=int(port) if port else None) finding.unsaved_endpoints = [location] finding.unsaved_vulnerability_ids = temp.get("cve_list", []) ret_rows.append(finding) From 8710573f66635f2dc94dbd6d001ba8c7a700286e Mon Sep 17 00:00:00 2001 From: Tejas Saubhage Date: Tue, 17 Mar 2026 00:24:59 -0400 Subject: [PATCH 3/4] Remove port from finding title, keep only in endpoint --- dojo/tools/qualys/parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dojo/tools/qualys/parser.py b/dojo/tools/qualys/parser.py index 7dedf32a4fc..2030ae7b124 100644 --- a/dojo/tools/qualys/parser.py +++ b/dojo/tools/qualys/parser.py @@ -320,7 +320,7 @@ def parse_finding(host, tree): if temp_cve_details: refs = temp.get("links", "") finding = Finding( - title="QID-" + gid[4:] + " | " + temp["vuln_name"] + (" | Port: " + str(temp["port_status"]) if temp.get("port_status") else ""), + title="QID-" + gid[4:] + " | " + temp["vuln_name"], mitigation=temp["solution"], description=temp["vuln_description"], severity=sev, @@ -332,7 +332,7 @@ def parse_finding(host, tree): else: finding = Finding( - title="QID-" + gid[4:] + " | " + temp["vuln_name"] + (" | Port: " + str(temp["port_status"]) if temp.get("port_status") else ""), + title="QID-" + gid[4:] + " | " + temp["vuln_name"], mitigation=temp["solution"], description=temp["vuln_description"], severity=sev, From 8104595cb58bc0702d032fba6aedfe7f046ebbff Mon Sep 17 00:00:00 2001 From: Tejas Saubhage Date: Fri, 20 Mar 2026 13:35:50 -0400 Subject: [PATCH 4/4] test(qualys): add unit test for same QID different ports deduplication fix - Add test XML with same QID on ports 80, 443, 8080 - Add test verifying each port gets its own endpoint - Add 2.57.x release notes mentioning the fix Addresses review feedback from @Maffooch on PR #14528 test(qualys): add unit test for same QID different ports deduplication fix - Add test XML with same QID on ports 80, 443, 8080 - Add test verifying each port gets its own endpoint - Add 2.59.x release notes mentioning the fix Addresses review feedback from @Maffooch on PR #14528 --- docs/content/releases/os_upgrading/2.59.md | 4 ++ .../qualys_same_qid_different_ports.xml | 68 +++++++++++++++++++ unittests/tools/test_qualys_parser.py | 26 +++++++ 3 files changed, 98 insertions(+) create mode 100644 unittests/scans/qualys/qualys_same_qid_different_ports.xml diff --git a/docs/content/releases/os_upgrading/2.59.md b/docs/content/releases/os_upgrading/2.59.md index c9921cf6be8..a496db52ae2 100644 --- a/docs/content/releases/os_upgrading/2.59.md +++ b/docs/content/releases/os_upgrading/2.59.md @@ -41,3 +41,7 @@ As announced in DefectDojo 2.57.0, the Stub Findings feature has been removed. T Any requests to this endpoint will now return a 404 Not Found error. The Stub Findings UI is no longer available. For more information, check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.59.0). + +## Bug Fixes + +- **Qualys Parser**: Fixed an issue where findings with the same QID but different ports were being collapsed into a single finding. Each QID+port combination now correctly gets its own endpoint, preserving port-level granularity without affecting finding titles or deduplication. ([#13682](https://github.com/DefectDojo/django-DefectDojo/issues/13682)) diff --git a/unittests/scans/qualys/qualys_same_qid_different_ports.xml b/unittests/scans/qualys/qualys_same_qid_different_ports.xml new file mode 100644 index 00000000000..9e4c7fe29d1 --- /dev/null +++ b/unittests/scans/qualys/qualys_same_qid_different_ports.xml @@ -0,0 +1,68 @@ + + +
+ + + + + 192.168.1.1 + 192.168.1.1 + + + +
+ + + + 12345 + <![CDATA[Test Vulnerability]]> + 3 + + 2024-01-01T00:00:00Z + + + + + + + + 192.168.1.1 + IP + + + 2024-01-01T00:00:00Z + + + 12345 + Practice + 80 + false + + 2024-01-01T00:00:00Z + 2024-01-01T00:00:00Z + 1 + + + 12345 + Practice + 443 + true + + 2024-01-01T00:00:00Z + 2024-01-01T00:00:00Z + 1 + + + 12345 + Practice + 8080 + false + + 2024-01-01T00:00:00Z + 2024-01-01T00:00:00Z + 1 + + + + +
\ No newline at end of file diff --git a/unittests/tools/test_qualys_parser.py b/unittests/tools/test_qualys_parser.py index 060b6b9fcc0..e8e6d838a78 100644 --- a/unittests/tools/test_qualys_parser.py +++ b/unittests/tools/test_qualys_parser.py @@ -239,3 +239,29 @@ def test_get_severity(self): } self.assertEqual(expected_counts, counts) + + def test_parse_file_same_qid_different_ports_has_separate_endpoints(self): + """Test that findings with same QID but different ports get separate endpoints. + Regression test for https://github.com/DefectDojo/django-DefectDojo/issues/13682 + """ + with ( + get_unit_tests_scans_path("qualys") / "qualys_same_qid_different_ports.xml").open(encoding="utf-8", + ) as testfile: + parser = QualysParser() + findings = parser.get_findings(testfile, Test()) + self.validate_locations(findings) + # Same QID on 3 different ports should produce 3 separate findings + self.assertEqual(3, len(findings)) + # All findings should have the same title (QID unchanged) + for finding in findings: + self.assertEqual(finding.title, "QID-12345 | Test Vulnerability") + # Each finding should have a different port on its endpoint + ports = set() + for finding in findings: + locations = self.get_unsaved_locations(finding) + self.assertEqual(1, len(locations)) + self.assertEqual(locations[0].host, "testhost.example.com") + ports.add(locations[0].port) + # All 3 ports should be present + self.assertEqual({80, 443, 8080}, ports) +