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
4 changes: 4 additions & 0 deletions docs/content/releases/os_upgrading/2.59.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
6 changes: 4 additions & 2 deletions dojo/tools/qualys/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
68 changes: 68 additions & 0 deletions unittests/scans/qualys/qualys_same_qid_different_ports.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<ASSET_DATA_REPORT>
<HEADER>
<COMPANY><![CDATA[Test Company]]></COMPANY>
<REPORT_FILTERS>
<COMBINED_IP_LIST>
<RANGE>
<START>192.168.1.1</START>
<END>192.168.1.1</END>
</RANGE>
</COMBINED_IP_LIST>
</REPORT_FILTERS>
</HEADER>
<GLOSSARY>
<VULN_DETAILS_LIST>
<VULN_DETAILS id="qid_12345">
<QID id="qid_12345">12345</QID>
<TITLE><![CDATA[Test Vulnerability]]></TITLE>
<SEVERITY>3</SEVERITY>
<CATEGORY><![CDATA[Test]]></CATEGORY>
<LAST_UPDATE>2024-01-01T00:00:00Z</LAST_UPDATE>
<DIAGNOSIS><![CDATA[Test diagnosis]]></DIAGNOSIS>
<SOLUTION><![CDATA[Test solution]]></SOLUTION>
</VULN_DETAILS>
</VULN_DETAILS_LIST>
</GLOSSARY>
<HOST_LIST>
<HOST>
<IP>192.168.1.1</IP>
<TRACKING_METHOD>IP</TRACKING_METHOD>
<DNS><![CDATA[testhost.example.com]]></DNS>
<OS><![CDATA[Linux]]></OS>
<LAST_SCAN_DATETIME>2024-01-01T00:00:00Z</LAST_SCAN_DATETIME>
<VULN_INFO_LIST>
<VULN_INFO>
<QID id="qid_12345">12345</QID>
<TYPE>Practice</TYPE>
<PORT>80</PORT>
<SSL>false</SSL>
<RESULT format="table"><![CDATA[Test result on port 80]]></RESULT>
<FIRST_FOUND>2024-01-01T00:00:00Z</FIRST_FOUND>
<LAST_FOUND>2024-01-01T00:00:00Z</LAST_FOUND>
<TIMES_FOUND>1</TIMES_FOUND>
</VULN_INFO>
<VULN_INFO>
<QID id="qid_12345">12345</QID>
<TYPE>Practice</TYPE>
<PORT>443</PORT>
<SSL>true</SSL>
<RESULT format="table"><![CDATA[Test result on port 443]]></RESULT>
<FIRST_FOUND>2024-01-01T00:00:00Z</FIRST_FOUND>
<LAST_FOUND>2024-01-01T00:00:00Z</LAST_FOUND>
<TIMES_FOUND>1</TIMES_FOUND>
</VULN_INFO>
<VULN_INFO>
<QID id="qid_12345">12345</QID>
<TYPE>Practice</TYPE>
<PORT>8080</PORT>
<SSL>false</SSL>
<RESULT format="table"><![CDATA[Test result on port 8080]]></RESULT>
<FIRST_FOUND>2024-01-01T00:00:00Z</FIRST_FOUND>
<LAST_FOUND>2024-01-01T00:00:00Z</LAST_FOUND>
<TIMES_FOUND>1</TIMES_FOUND>
</VULN_INFO>
</VULN_INFO_LIST>
</HOST>
</HOST_LIST>
</ASSET_DATA_REPORT>
26 changes: 26 additions & 0 deletions unittests/tools/test_qualys_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,29 @@
}

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
"""

Check failure on line 246 in unittests/tools/test_qualys_parser.py

View workflow job for this annotation

GitHub Actions / ruff-linting

ruff (D213)

unittests/tools/test_qualys_parser.py:244:9: D213 Multi-line docstring summary should start at the second line help: Insert line break and indentation after opening quotes
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)

Loading