From 8f1d14101e4510095c82c61ec8d1769359004808 Mon Sep 17 00:00:00 2001 From: Paul Osinski Date: Mon, 30 Mar 2026 11:51:35 -0400 Subject: [PATCH 1/5] update invicti parser to use FirstSeenDate --- dojo/tools/netsparker/parser.py | 37 ++++++++++++++--------- unittests/tools/test_invicti_parser.py | 8 ++--- unittests/tools/test_netsparker_parser.py | 10 +++--- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/dojo/tools/netsparker/parser.py b/dojo/tools/netsparker/parser.py index e53c67e241b..5a7d43dcbde 100644 --- a/dojo/tools/netsparker/parser.py +++ b/dojo/tools/netsparker/parser.py @@ -10,6 +10,25 @@ from dojo.tools.locations import LocationData +def _parse_date(date_str): + """Parse a Netsparker/Invicti date string into a date object.""" + if not date_str: + return None + try: + if "UTC" in date_str: + return datetime.datetime.strptime( + date_str.split(" ")[0], "%d/%m/%Y", + ).date() + return datetime.datetime.strptime( + date_str, "%d/%m/%Y %H:%M %p", + ).date() + except ValueError: + try: + return date_parser.parse(date_str).date() + except (ValueError, date_parser.ParserError): + return None + + class NetsparkerParser: def get_scan_types(self): return ["Netsparker Scan"] @@ -27,20 +46,7 @@ def get_findings(self, filename, test): except Exception: data = json.loads(tree) dupes = {} - try: - if "UTC" in data["Generated"]: - scan_date = datetime.datetime.strptime( - data["Generated"].split(" ")[0], "%d/%m/%Y", - ).date() - else: - scan_date = datetime.datetime.strptime( - data["Generated"], "%d/%m/%Y %H:%M %p", - ).date() - except ValueError: - try: - scan_date = date_parser.parse(data["Generated"]) - except date_parser.ParserError: - scan_date = None + scan_date = _parse_date(data.get("Generated")) for item in data["Vulnerabilities"]: title = item["Name"] @@ -62,6 +68,7 @@ def get_findings(self, filename, test): dupe_key = title request = item["HttpRequest"].get("Content", None) response = item["HttpResponse"].get("Content", None) + finding_date = _parse_date(item.get("FirstSeenDate")) or scan_date finding = Finding( title=title, @@ -70,7 +77,7 @@ def get_findings(self, filename, test): severity=sev.title(), mitigation=mitigation, impact=impact, - date=scan_date, + date=finding_date, references=references, cwe=cwe, static_finding=True, diff --git a/unittests/tools/test_invicti_parser.py b/unittests/tools/test_invicti_parser.py index d269ac55175..393415c1a0f 100644 --- a/unittests/tools/test_invicti_parser.py +++ b/unittests/tools/test_invicti_parser.py @@ -16,7 +16,7 @@ def test_parse_file_with_one_finding(self): finding = findings[0] self.assertEqual("Medium", finding.severity) self.assertEqual(16, finding.cwe) - self.assertEqual("25/06/2021", finding.date.strftime("%d/%m/%Y")) + self.assertEqual("16/06/2021", finding.date.strftime("%d/%m/%Y")) self.assertIsNotNone(finding.description) self.assertGreater(len(finding.description), 0) self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:N/A:N/E:H/RL:O/RC:C", finding.cvssv3) @@ -34,7 +34,7 @@ def test_parse_file_with_multiple_finding(self): finding = findings[0] self.assertEqual("Medium", finding.severity) self.assertEqual(16, finding.cwe) - self.assertEqual("25/06/2021", finding.date.strftime("%d/%m/%Y")) + self.assertEqual("16/06/2021", finding.date.strftime("%d/%m/%Y")) self.assertIsNotNone(finding.description) self.assertGreater(len(finding.description), 0) self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:N/A:N/E:H/RL:O/RC:C", finding.cvssv3) @@ -46,7 +46,7 @@ def test_parse_file_with_multiple_finding(self): finding = findings[1] self.assertEqual("Critical", finding.severity) self.assertEqual(89, finding.cwe) - self.assertEqual("25/06/2021", finding.date.strftime("%d/%m/%Y")) + self.assertEqual("16/06/2021", finding.date.strftime("%d/%m/%Y")) self.assertIsNotNone(finding.description) self.assertGreater(len(finding.description), 0) self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", finding.cvssv3) @@ -58,7 +58,7 @@ def test_parse_file_with_multiple_finding(self): finding = findings[2] self.assertEqual("Medium", finding.severity) self.assertEqual(205, finding.cwe) - self.assertEqual("25/06/2021", finding.date.strftime("%d/%m/%Y")) + self.assertEqual("15/06/2021", finding.date.strftime("%d/%m/%Y")) self.assertIsNotNone(finding.description) self.assertGreater(len(finding.description), 0) self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N/E:H/RL:O/RC:C", finding.cvssv3) diff --git a/unittests/tools/test_netsparker_parser.py b/unittests/tools/test_netsparker_parser.py index b18ace0fa2b..4888e8ca3ca 100644 --- a/unittests/tools/test_netsparker_parser.py +++ b/unittests/tools/test_netsparker_parser.py @@ -16,7 +16,7 @@ def test_parse_file_with_one_finding(self): finding = findings[0] self.assertEqual("Medium", finding.severity) self.assertEqual(16, finding.cwe) - self.assertEqual("25/06/2021", finding.date.strftime("%d/%m/%Y")) + self.assertEqual("16/06/2021", finding.date.strftime("%d/%m/%Y")) self.assertIsNotNone(finding.description) self.assertGreater(len(finding.description), 0) self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:N/A:N/E:H/RL:O/RC:C", finding.cvssv3) @@ -34,7 +34,7 @@ def test_parse_file_with_multiple_finding(self): finding = findings[0] self.assertEqual("Medium", finding.severity) self.assertEqual(16, finding.cwe) - self.assertEqual("25/06/2021", finding.date.strftime("%d/%m/%Y")) + self.assertEqual("16/06/2021", finding.date.strftime("%d/%m/%Y")) self.assertIsNotNone(finding.description) self.assertGreater(len(finding.description), 0) self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:N/A:N/E:H/RL:O/RC:C", finding.cvssv3) @@ -46,7 +46,7 @@ def test_parse_file_with_multiple_finding(self): finding = findings[1] self.assertEqual("Critical", finding.severity) self.assertEqual(89, finding.cwe) - self.assertEqual("25/06/2021", finding.date.strftime("%d/%m/%Y")) + self.assertEqual("16/06/2021", finding.date.strftime("%d/%m/%Y")) self.assertIsNotNone(finding.description) self.assertGreater(len(finding.description), 0) self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", finding.cvssv3) @@ -58,7 +58,7 @@ def test_parse_file_with_multiple_finding(self): finding = findings[2] self.assertEqual("Medium", finding.severity) self.assertEqual(205, finding.cwe) - self.assertEqual("25/06/2021", finding.date.strftime("%d/%m/%Y")) + self.assertEqual("15/06/2021", finding.date.strftime("%d/%m/%Y")) self.assertIsNotNone(finding.description) self.assertGreater(len(finding.description), 0) self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N/E:H/RL:O/RC:C", finding.cvssv3) @@ -100,4 +100,4 @@ def test_parse_file_issue_11020(self): finding = findings[0] self.assertEqual("Low", finding.severity) self.assertEqual(205, finding.cwe) - self.assertEqual("08/10/2024", finding.date.strftime("%d/%m/%Y")) + self.assertEqual("23/07/2024", finding.date.strftime("%d/%m/%Y")) From 114c1ed9ad800a29428d2812111c5e2f21c80aa2 Mon Sep 17 00:00:00 2001 From: Paul Osinski Date: Tue, 7 Apr 2026 14:47:28 -0400 Subject: [PATCH 2/5] invicti: respect USE_FIRST_SEEN --- dojo/tools/netsparker/parser.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dojo/tools/netsparker/parser.py b/dojo/tools/netsparker/parser.py index 5a7d43dcbde..dfb10ced049 100644 --- a/dojo/tools/netsparker/parser.py +++ b/dojo/tools/netsparker/parser.py @@ -68,7 +68,10 @@ def get_findings(self, filename, test): dupe_key = title request = item["HttpRequest"].get("Content", None) response = item["HttpResponse"].get("Content", None) - finding_date = _parse_date(item.get("FirstSeenDate")) or scan_date + if settings.USE_FIRST_SEEN: + finding_date = _parse_date(item.get("FirstSeenDate")) or scan_date + else: + finding_date = scan_date finding = Finding( title=title, From 412bada9755cc01fce795cdb998defd29efd8ca0 Mon Sep 17 00:00:00 2001 From: Paul Osinski Date: Tue, 7 Apr 2026 14:55:34 -0400 Subject: [PATCH 3/5] update invicti test --- unittests/tools/test_netsparker_parser.py | 59 +++++++++++++++++++++-- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/unittests/tools/test_netsparker_parser.py b/unittests/tools/test_netsparker_parser.py index 4888e8ca3ca..276fc977dd8 100644 --- a/unittests/tools/test_netsparker_parser.py +++ b/unittests/tools/test_netsparker_parser.py @@ -1,4 +1,6 @@ +from django.test import override_settings + from dojo.models import Test from dojo.tools.netsparker.parser import NetsparkerParser from unittests.dojo_test_case import DojoTestCase, get_unit_tests_scans_path @@ -7,6 +9,7 @@ class TestNetsparkerParser(DojoTestCase): def test_parse_file_with_one_finding(self): + """With USE_FIRST_SEEN=False (default), date should come from Generated (scan date).""" with (get_unit_tests_scans_path("netsparker") / "netsparker_one_finding.json").open(encoding="utf-8") as testfile: parser = NetsparkerParser() findings = parser.get_findings(testfile, Test()) @@ -16,7 +19,8 @@ def test_parse_file_with_one_finding(self): finding = findings[0] self.assertEqual("Medium", finding.severity) self.assertEqual(16, finding.cwe) - self.assertEqual("16/06/2021", finding.date.strftime("%d/%m/%Y")) + # Generated date is "25/06/2021 09:59 AM" + self.assertEqual("25/06/2021", finding.date.strftime("%d/%m/%Y")) self.assertIsNotNone(finding.description) self.assertGreater(len(finding.description), 0) self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:N/A:N/E:H/RL:O/RC:C", finding.cvssv3) @@ -24,7 +28,23 @@ def test_parse_file_with_one_finding(self): location = self.get_unsaved_locations(finding)[0] self.assertEqual(str(location), "http://php.testsparker.com/auth/login.php") + @override_settings(USE_FIRST_SEEN=True) + def test_parse_file_with_one_finding_first_seen(self): + """With USE_FIRST_SEEN=True, date should come from FirstSeenDate.""" + with (get_unit_tests_scans_path("netsparker") / "netsparker_one_finding.json").open(encoding="utf-8") as testfile: + parser = NetsparkerParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(1, len(findings)) + self.validate_locations(findings) + with self.subTest(i=0): + finding = findings[0] + self.assertEqual("Medium", finding.severity) + self.assertEqual(16, finding.cwe) + # FirstSeenDate is "16/06/2021 12:30 PM" + self.assertEqual("16/06/2021", finding.date.strftime("%d/%m/%Y")) + def test_parse_file_with_multiple_finding(self): + """With USE_FIRST_SEEN=False (default), dates should come from Generated (scan date).""" with (get_unit_tests_scans_path("netsparker") / "netsparker_many_findings.json").open(encoding="utf-8") as testfile: parser = NetsparkerParser() findings = parser.get_findings(testfile, Test()) @@ -34,7 +54,8 @@ def test_parse_file_with_multiple_finding(self): finding = findings[0] self.assertEqual("Medium", finding.severity) self.assertEqual(16, finding.cwe) - self.assertEqual("16/06/2021", finding.date.strftime("%d/%m/%Y")) + # Generated date is "25/06/2021 10:00 AM" + self.assertEqual("25/06/2021", finding.date.strftime("%d/%m/%Y")) self.assertIsNotNone(finding.description) self.assertGreater(len(finding.description), 0) self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:N/A:N/E:H/RL:O/RC:C", finding.cvssv3) @@ -46,7 +67,7 @@ def test_parse_file_with_multiple_finding(self): finding = findings[1] self.assertEqual("Critical", finding.severity) self.assertEqual(89, finding.cwe) - self.assertEqual("16/06/2021", finding.date.strftime("%d/%m/%Y")) + self.assertEqual("25/06/2021", finding.date.strftime("%d/%m/%Y")) self.assertIsNotNone(finding.description) self.assertGreater(len(finding.description), 0) self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", finding.cvssv3) @@ -58,7 +79,7 @@ def test_parse_file_with_multiple_finding(self): finding = findings[2] self.assertEqual("Medium", finding.severity) self.assertEqual(205, finding.cwe) - self.assertEqual("15/06/2021", finding.date.strftime("%d/%m/%Y")) + self.assertEqual("25/06/2021", finding.date.strftime("%d/%m/%Y")) self.assertIsNotNone(finding.description) self.assertGreater(len(finding.description), 0) self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N/E:H/RL:O/RC:C", finding.cvssv3) @@ -66,6 +87,22 @@ def test_parse_file_with_multiple_finding(self): location = self.get_unsaved_locations(finding)[0] self.assertEqual(str(location), "http://php.testsparker.com") + @override_settings(USE_FIRST_SEEN=True) + def test_parse_file_with_multiple_finding_first_seen(self): + """With USE_FIRST_SEEN=True, dates should come from FirstSeenDate.""" + with (get_unit_tests_scans_path("netsparker") / "netsparker_many_findings.json").open(encoding="utf-8") as testfile: + parser = NetsparkerParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(16, len(findings)) + with self.subTest(i=0): + finding = findings[0] + # FirstSeenDate is "16/06/2021 12:30 PM" + self.assertEqual("16/06/2021", finding.date.strftime("%d/%m/%Y")) + with self.subTest(i=2): + finding = findings[2] + # FirstSeenDate is "15/06/2021 01:44 PM" + self.assertEqual("15/06/2021", finding.date.strftime("%d/%m/%Y")) + def test_parse_file_issue_9816(self): with (get_unit_tests_scans_path("netsparker") / "issue_9816.json").open(encoding="utf-8") as testfile: parser = NetsparkerParser() @@ -91,6 +128,7 @@ def test_parse_file_issue_10311(self): self.assertEqual("03/02/2019", finding.date.strftime("%d/%m/%Y")) def test_parse_file_issue_11020(self): + """With USE_FIRST_SEEN=False (default), date should come from Generated (scan date).""" with (get_unit_tests_scans_path("netsparker") / "issue_11020.json").open(encoding="utf-8") as testfile: parser = NetsparkerParser() findings = parser.get_findings(testfile, Test()) @@ -100,4 +138,17 @@ def test_parse_file_issue_11020(self): finding = findings[0] self.assertEqual("Low", finding.severity) self.assertEqual(205, finding.cwe) + # Generated date is "2024-10-08 02:33 PM" + self.assertEqual("08/10/2024", finding.date.strftime("%d/%m/%Y")) + + @override_settings(USE_FIRST_SEEN=True) + def test_parse_file_issue_11020_first_seen(self): + """With USE_FIRST_SEEN=True, date should come from FirstSeenDate.""" + with (get_unit_tests_scans_path("netsparker") / "issue_11020.json").open(encoding="utf-8") as testfile: + parser = NetsparkerParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(3, len(findings)) + with self.subTest(i=0): + finding = findings[0] + # FirstSeenDate is "2024-07-23 05:32 PM" self.assertEqual("23/07/2024", finding.date.strftime("%d/%m/%Y")) From e734d0851087ceb6b12dd4dd6e106bafbff21b87 Mon Sep 17 00:00:00 2001 From: Paul Osinski Date: Tue, 7 Apr 2026 15:08:48 -0400 Subject: [PATCH 4/5] ruff --- dojo/tools/netsparker/parser.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dojo/tools/netsparker/parser.py b/dojo/tools/netsparker/parser.py index dfb10ced049..1859b21ebbe 100644 --- a/dojo/tools/netsparker/parser.py +++ b/dojo/tools/netsparker/parser.py @@ -68,10 +68,7 @@ def get_findings(self, filename, test): dupe_key = title request = item["HttpRequest"].get("Content", None) response = item["HttpResponse"].get("Content", None) - if settings.USE_FIRST_SEEN: - finding_date = _parse_date(item.get("FirstSeenDate")) or scan_date - else: - finding_date = scan_date + finding_date = (_parse_date(item.get("FirstSeenDate")) or scan_date) if settings.USE_FIRST_SEEN else scan_date finding = Finding( title=title, From 242b0c59a9260a99a132764bd443bf47f1125c3a Mon Sep 17 00:00:00 2001 From: Paul Osinski Date: Tue, 7 Apr 2026 17:07:37 -0400 Subject: [PATCH 5/5] update invicti test to use Generated scan date --- unittests/tools/test_invicti_parser.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/unittests/tools/test_invicti_parser.py b/unittests/tools/test_invicti_parser.py index 393415c1a0f..d269ac55175 100644 --- a/unittests/tools/test_invicti_parser.py +++ b/unittests/tools/test_invicti_parser.py @@ -16,7 +16,7 @@ def test_parse_file_with_one_finding(self): finding = findings[0] self.assertEqual("Medium", finding.severity) self.assertEqual(16, finding.cwe) - self.assertEqual("16/06/2021", finding.date.strftime("%d/%m/%Y")) + self.assertEqual("25/06/2021", finding.date.strftime("%d/%m/%Y")) self.assertIsNotNone(finding.description) self.assertGreater(len(finding.description), 0) self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:N/A:N/E:H/RL:O/RC:C", finding.cvssv3) @@ -34,7 +34,7 @@ def test_parse_file_with_multiple_finding(self): finding = findings[0] self.assertEqual("Medium", finding.severity) self.assertEqual(16, finding.cwe) - self.assertEqual("16/06/2021", finding.date.strftime("%d/%m/%Y")) + self.assertEqual("25/06/2021", finding.date.strftime("%d/%m/%Y")) self.assertIsNotNone(finding.description) self.assertGreater(len(finding.description), 0) self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:N/A:N/E:H/RL:O/RC:C", finding.cvssv3) @@ -46,7 +46,7 @@ def test_parse_file_with_multiple_finding(self): finding = findings[1] self.assertEqual("Critical", finding.severity) self.assertEqual(89, finding.cwe) - self.assertEqual("16/06/2021", finding.date.strftime("%d/%m/%Y")) + self.assertEqual("25/06/2021", finding.date.strftime("%d/%m/%Y")) self.assertIsNotNone(finding.description) self.assertGreater(len(finding.description), 0) self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", finding.cvssv3) @@ -58,7 +58,7 @@ def test_parse_file_with_multiple_finding(self): finding = findings[2] self.assertEqual("Medium", finding.severity) self.assertEqual(205, finding.cwe) - self.assertEqual("15/06/2021", finding.date.strftime("%d/%m/%Y")) + self.assertEqual("25/06/2021", finding.date.strftime("%d/%m/%Y")) self.assertIsNotNone(finding.description) self.assertGreater(len(finding.description), 0) self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N/E:H/RL:O/RC:C", finding.cvssv3)