From 45adce53acce35dfe4967acbf87a8aea5f1bbd0a Mon Sep 17 00:00:00 2001 From: DefectDojo release bot Date: Mon, 15 Sep 2025 15:17:53 +0000 Subject: [PATCH 01/13] 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 c6880683761..d96e5e51133 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "defectdojo", - "version": "2.50.2", + "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 3931a9c6f43..41f37583caa 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 -appVersion: "2.50.2" +appVersion: "2.51.0-dev" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.6.207 +version: 1.6.208-dev icon: https://www.defectdojo.org/img/favicon.ico maintainers: - name: madchap From 1a80117b1d1a31f74dd27c303bc23764f20880f8 Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Mon, 15 Sep 2025 18:56:13 +0200 Subject: [PATCH 02/13] product grade: only update product if grade has changed (#13123) * product grade: only update product if grade has changed * product grade: only update product if grade has changed --- dojo/utils.py | 9 +++++++-- unittests/test_importers_performance.py | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/dojo/utils.py b/dojo/utils.py index 991b6b84075..131c9ef11a5 100644 --- a/dojo/utils.py +++ b/dojo/utils.py @@ -1598,8 +1598,13 @@ def calculate_grade(product, *args, **kwargs): aeval = Interpreter() aeval(system_settings.product_grade) grade_product = f"grade_product({critical}, {high}, {medium}, {low})" - product.prod_numeric_grade = aeval(grade_product) - super(Product, product).save() + prod_numeric_grade = aeval(grade_product) + if prod_numeric_grade != product.prod_numeric_grade: + logger.debug("Updating product %s grade from %s to %s", product.id, product.prod_numeric_grade, prod_numeric_grade) + 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) def get_celery_worker_status(): diff --git a/unittests/test_importers_performance.py b/unittests/test_importers_performance.py index 4f316d7350a..4aeba1a2adf 100644 --- a/unittests/test_importers_performance.py +++ b/unittests/test_importers_performance.py @@ -201,10 +201,10 @@ def test_import_reimport_reimport_performance_no_async_with_product_grading(self DojoSytemSettingsMiddleware.load() self.import_reimport_performance( - expected_num_queries1=732, + expected_num_queries1=717, expected_num_async_tasks1=15, - expected_num_queries2=686, + expected_num_queries2=662, expected_num_async_tasks2=28, - expected_num_queries3=357, + expected_num_queries3=337, expected_num_async_tasks3=25, ) From 57069ba343b4cc43a6acbb48a95f7ce5c01349be Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Wed, 17 Sep 2025 04:37:40 +0200 Subject: [PATCH 03/13] Fix naive datetime warnings/errors (#13170) * test filter on last_status_update * test filter on last_status_update * make datetimes timezone aware --- dojo/engagement/views.py | 4 ++ dojo/filters.py | 68 +++++++++++++------------- dojo/metrics/utils.py | 2 + dojo/test/views.py | 4 ++ dojo/tools/cyberwatch_galeax/parser.py | 15 +++--- dojo/utils.py | 21 +++++++- tests/check_various_pages.py | 6 +++ 7 files changed, 78 insertions(+), 42 deletions(-) diff --git a/dojo/engagement/views.py b/dojo/engagement/views.py index 29faadb639e..a06f32e8441 100644 --- a/dojo/engagement/views.py +++ b/dojo/engagement/views.py @@ -1552,6 +1552,10 @@ def engagement_ics(request, eid): eng = get_object_or_404(Engagement, id=eid) start_date = datetime.combine(eng.target_start, datetime.min.time()) end_date = datetime.combine(eng.target_end, datetime.max.time()) + if timezone.is_naive(start_date): + start_date = timezone.make_aware(start_date) + if timezone.is_naive(end_date): + end_date = timezone.make_aware(end_date) uid = f"dojo_eng_{eng.id}_{eng.product.id}" cal = get_cal_event( start_date, diff --git a/dojo/filters.py b/dojo/filters.py index 940d1f970f5..c79bf952f21 100644 --- a/dojo/filters.py +++ b/dojo/filters.py @@ -30,7 +30,7 @@ RangeFilter, ) from django_filters import rest_framework as filters -from django_filters.filters import ChoiceFilter, _truncate +from django_filters.filters import ChoiceFilter from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from polymorphic.base import ManagerInheritanceWarning @@ -92,7 +92,7 @@ from dojo.risk_acceptance.queries import get_authorized_risk_acceptances from dojo.test.queries import get_authorized_tests from dojo.user.queries import get_authorized_users -from dojo.utils import get_system_setting, is_finding_groups_enabled +from dojo.utils import get_system_setting, is_finding_groups_enabled, truncate_timezone_aware logger = logging.getLogger(__name__) @@ -194,8 +194,8 @@ def filter(self, qs, value): if earliest_finding is not None: start_date = datetime.combine( earliest_finding.date, datetime.min.time()).replace(tzinfo=tzinfo()) - self.start_date = _truncate(start_date - timedelta(days=1)) - self.end_date = _truncate(now() + timedelta(days=1)) + self.start_date = truncate_timezone_aware(start_date - timedelta(days=1)) + self.end_date = truncate_timezone_aware(now() + timedelta(days=1)) try: value = int(value) except (ValueError, TypeError): @@ -654,16 +654,16 @@ class DateRangeFilter(ChoiceFilter): f"{name}__day": now().day, })), 2: (_("Past 7 days"), lambda qs, name: qs.filter(**{ - f"{name}__gte": _truncate(now() - timedelta(days=7)), - f"{name}__lt": _truncate(now() + timedelta(days=1)), + f"{name}__gte": truncate_timezone_aware(now() - timedelta(days=7)), + f"{name}__lt": truncate_timezone_aware(now() + timedelta(days=1)), })), 3: (_("Past 30 days"), lambda qs, name: qs.filter(**{ - f"{name}__gte": _truncate(now() - timedelta(days=30)), - f"{name}__lt": _truncate(now() + timedelta(days=1)), + f"{name}__gte": truncate_timezone_aware(now() - timedelta(days=30)), + f"{name}__lt": truncate_timezone_aware(now() + timedelta(days=1)), })), 4: (_("Past 90 days"), lambda qs, name: qs.filter(**{ - f"{name}__gte": _truncate(now() - timedelta(days=90)), - f"{name}__lt": _truncate(now() + timedelta(days=1)), + f"{name}__gte": truncate_timezone_aware(now() - timedelta(days=90)), + f"{name}__lt": truncate_timezone_aware(now() + timedelta(days=1)), })), 5: (_("Current month"), lambda qs, name: qs.filter(**{ f"{name}__year": now().year, @@ -673,8 +673,8 @@ class DateRangeFilter(ChoiceFilter): f"{name}__year": now().year, })), 7: (_("Past year"), lambda qs, name: qs.filter(**{ - f"{name}__gte": _truncate(now() - timedelta(days=365)), - f"{name}__lt": _truncate(now() + timedelta(days=1)), + f"{name}__gte": truncate_timezone_aware(now() - timedelta(days=365)), + f"{name}__lt": truncate_timezone_aware(now() + timedelta(days=1)), })), } @@ -700,43 +700,43 @@ class DateRangeOmniFilter(ChoiceFilter): f"{name}__day": now().day, })), 2: (_("Next 7 days"), lambda qs, name: qs.filter(**{ - f"{name}__gte": _truncate(now() + timedelta(days=1)), - f"{name}__lt": _truncate(now() + timedelta(days=7)), + f"{name}__gte": truncate_timezone_aware(now() + timedelta(days=1)), + f"{name}__lt": truncate_timezone_aware(now() + timedelta(days=7)), })), 3: (_("Next 30 days"), lambda qs, name: qs.filter(**{ - f"{name}__gte": _truncate(now() + timedelta(days=1)), - f"{name}__lt": _truncate(now() + timedelta(days=30)), + f"{name}__gte": truncate_timezone_aware(now() + timedelta(days=1)), + f"{name}__lt": truncate_timezone_aware(now() + timedelta(days=30)), })), 4: (_("Next 90 days"), lambda qs, name: qs.filter(**{ - f"{name}__gte": _truncate(now() + timedelta(days=1)), - f"{name}__lt": _truncate(now() + timedelta(days=90)), + f"{name}__gte": truncate_timezone_aware(now() + timedelta(days=1)), + f"{name}__lt": truncate_timezone_aware(now() + timedelta(days=90)), })), 5: (_("Past 7 days"), lambda qs, name: qs.filter(**{ - f"{name}__gte": _truncate(now() - timedelta(days=7)), - f"{name}__lt": _truncate(now() + timedelta(days=1)), + f"{name}__gte": truncate_timezone_aware(now() - timedelta(days=7)), + f"{name}__lt": truncate_timezone_aware(now() + timedelta(days=1)), })), 6: (_("Past 30 days"), lambda qs, name: qs.filter(**{ - f"{name}__gte": _truncate(now() - timedelta(days=30)), - f"{name}__lt": _truncate(now() + timedelta(days=1)), + f"{name}__gte": truncate_timezone_aware(now() - timedelta(days=30)), + f"{name}__lt": truncate_timezone_aware(now() + timedelta(days=1)), })), 7: (_("Past 90 days"), lambda qs, name: qs.filter(**{ - f"{name}__gte": _truncate(now() - timedelta(days=90)), - f"{name}__lt": _truncate(now() + timedelta(days=1)), + f"{name}__gte": truncate_timezone_aware(now() - timedelta(days=90)), + f"{name}__lt": truncate_timezone_aware(now() + timedelta(days=1)), })), 8: (_("Current month"), lambda qs, name: qs.filter(**{ f"{name}__year": now().year, f"{name}__month": now().month, })), 9: (_("Past year"), lambda qs, name: qs.filter(**{ - f"{name}__gte": _truncate(now() - timedelta(days=365)), - f"{name}__lt": _truncate(now() + timedelta(days=1)), + f"{name}__gte": truncate_timezone_aware(now() - timedelta(days=365)), + f"{name}__lt": truncate_timezone_aware(now() + timedelta(days=1)), })), 10: (_("Current year"), lambda qs, name: qs.filter(**{ f"{name}__year": now().year, })), 11: (_("Next year"), lambda qs, name: qs.filter(**{ - f"{name}__gte": _truncate(now() + timedelta(days=1)), - f"{name}__lt": _truncate(now() + timedelta(days=365)), + f"{name}__gte": truncate_timezone_aware(now() + timedelta(days=1)), + f"{name}__lt": truncate_timezone_aware(now() + timedelta(days=365)), })), } @@ -818,8 +818,8 @@ def any(self, qs, name): if earliest_finding is not None: start_date = datetime.combine( earliest_finding.date, datetime.min.time()).replace(tzinfo=tzinfo()) - self.start_date = _truncate(start_date - timedelta(days=1)) - self.end_date = _truncate(now() + timedelta(days=1)) + self.start_date = truncate_timezone_aware(start_date - timedelta(days=1)) + self.end_date = truncate_timezone_aware(now() + timedelta(days=1)) return qs.all() return None @@ -839,8 +839,8 @@ def current_year(self, qs, name): }) def past_x_days(self, qs, name, days): - self.start_date = _truncate(now() - timedelta(days=days)) - self.end_date = _truncate(now() + timedelta(days=1)) + self.start_date = truncate_timezone_aware(now() - timedelta(days=days)) + self.end_date = truncate_timezone_aware(now() + timedelta(days=1)) return qs.filter(**{ f"{name}__gte": self.start_date, f"{name}__lt": self.end_date, @@ -884,8 +884,8 @@ def filter(self, qs, value): if earliest_finding is not None: start_date = datetime.combine( earliest_finding.date, datetime.min.time()).replace(tzinfo=tzinfo()) - self.start_date = _truncate(start_date - timedelta(days=1)) - self.end_date = _truncate(now() + timedelta(days=1)) + self.start_date = truncate_timezone_aware(start_date - timedelta(days=1)) + self.end_date = truncate_timezone_aware(now() + timedelta(days=1)) try: value = int(value) except (ValueError, TypeError): diff --git a/dojo/metrics/utils.py b/dojo/metrics/utils.py index cf3e2813e13..4b98d7ec4ee 100644 --- a/dojo/metrics/utils.py +++ b/dojo/metrics/utils.py @@ -404,6 +404,8 @@ def js_epoch( """ if isinstance(d, date): d = datetime.combine(d, datetime.min.time()) + if timezone.is_naive(d): + d = timezone.make_aware(d) return int(d.timestamp()) * 1000 diff --git a/dojo/test/views.py b/dojo/test/views.py index 46af27b444a..06301d20813 100644 --- a/dojo/test/views.py +++ b/dojo/test/views.py @@ -405,6 +405,10 @@ def test_ics(request, tid): test = get_object_or_404(Test, id=tid) start_date = datetime.combine(test.target_start, datetime.min.time()) end_date = datetime.combine(test.target_end, datetime.max.time()) + if timezone.is_naive(start_date): + start_date = timezone.make_aware(start_date) + if timezone.is_naive(end_date): + end_date = timezone.make_aware(end_date) uid = f"dojo_test_{test.id}_{test.engagement.id}_{test.engagement.product.id}" cal = get_cal_event( start_date, diff --git a/dojo/tools/cyberwatch_galeax/parser.py b/dojo/tools/cyberwatch_galeax/parser.py index 02c0d6f85ea..c8ab5c1f66a 100644 --- a/dojo/tools/cyberwatch_galeax/parser.py +++ b/dojo/tools/cyberwatch_galeax/parser.py @@ -5,6 +5,7 @@ import cvss.parser from cvss.cvss3 import CVSS3 +from django.utils import timezone from dojo.models import Endpoint, Endpoint_Status, Finding @@ -202,7 +203,7 @@ def build_findings_for_cve(self, cve_code, c_data, test): products = c_data["products"] if not products: - mitigated_date = datetime.now() + mitigated_date = timezone.now() mitigation = f"Fixed At: {mitigated_date}" endpoints = [Endpoint(host=e) for e in c_data["no_product_endpoints"]] @@ -269,7 +270,7 @@ def determine_product_finding_state(self, p_data): active_status = any(am[0] for am in p_data["active_mitigated_data"]) mitigated_date = (max(am[1] for am in p_data["active_mitigated_data"] if am[1]) if [am[1] for am in p_data["active_mitigated_data"] if am[1]] and not active_status - else (datetime.now() if not active_status else None)) + else (timezone.now() if not active_status else None)) return component_version_str, active_status, mitigated_date def create_finding( @@ -466,7 +467,7 @@ def process_servers_for_security_issue(self, servers): active_status = True mitigated_date = None else: - mitigated_date = datetime.now() + mitigated_date = timezone.now() mitigated_dates.append(mitigated_date) detected_at_str = server.get("detected_at") @@ -483,7 +484,7 @@ def process_servers_for_security_issue(self, servers): ) unsaved_endpoint_status.append(endpoint_status) - mitigated_date = (max(mitigated_dates) if mitigated_dates else datetime.now()) if not active_status else None + mitigated_date = (max(mitigated_dates) if mitigated_dates else timezone.now()) if not active_status else None return unsaved_endpoints, unsaved_endpoint_status, active_status, mitigated_date def parse_detected_at(self, detected_at_str): @@ -491,7 +492,7 @@ def parse_detected_at(self, detected_at_str): try: return datetime.strptime(detected_at_str, "%Y-%m-%dT%H:%M:%S.%fZ") except (ValueError, TypeError): - return datetime.now() + return timezone.now() def parse_fixed_at(self, fixed_at_str): """Parse fixed_at datetime, defaulting to now if parsing fails.""" @@ -500,7 +501,7 @@ def parse_fixed_at(self, fixed_at_str): return datetime.strptime(fixed_at_str, "%Y-%m-%dT%H:%M:%S.%f%z") except ValueError as e: logger.error(f'Error parsing fixed_at date "{fixed_at_str}": {e}') - return datetime.now() + return timezone.now() def parse_datetime(self, dt_str): """Parse a datetime string with fallback to now on error.""" @@ -509,7 +510,7 @@ def parse_datetime(self, dt_str): return datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%S.%f%z") except (ValueError, TypeError): logger.error(f'Error parsing datetime "{dt_str}"') - return datetime.now() + return timezone.now() def parse_cvss(self, cvss_v3_vector, json_data): if cvss_v3_vector: diff --git a/dojo/utils.py b/dojo/utils.py index 131c9ef11a5..b11176a10ab 100644 --- a/dojo/utils.py +++ b/dojo/utils.py @@ -2470,7 +2470,8 @@ def get_open_findings_burndown(product): findings = Finding.objects.filter(test__engagement__product=product, duplicate=False) f_list = list(findings) - curr_date = datetime.combine(datetime.now(), datetime.min.time()) + curr_date = datetime.combine(timezone.now().date(), datetime.min.time()) + curr_date = timezone.make_aware(curr_date) start_date = curr_date - timedelta(days=90) critical_count = 0 @@ -2708,3 +2709,21 @@ def parse_cvss_data(cvss_vector_string: str) -> dict: } logger.debug("No valid CVSS3 or CVSS4 vector found in %s", cvss_vector_string) return {} + + +def truncate_timezone_aware(dt): + """ + Truncate datetime to date and make it timezone-aware. + This replaces the django_filters._truncate function which creates naive datetimes. + """ + if dt is None: + return None + + # Get the date part and create a new datetime at midnight + truncated = datetime.combine(dt.date(), datetime.min.time()) + + # Make it timezone-aware if it isn't already + if timezone.is_naive(truncated): + truncated = timezone.make_aware(truncated) + + return truncated diff --git a/tests/check_various_pages.py b/tests/check_various_pages.py index c78c7c596d0..aa1188253cc 100644 --- a/tests/check_various_pages.py +++ b/tests/check_various_pages.py @@ -32,6 +32,11 @@ def test_finding_group_open_filtered_status(self): driver = self.driver driver.get(self.base_url + "finding_group/open?name=CVE&severity=Medium&engagement=14&product=6") + def test_date_filter(self): + driver = self.driver + # can result in an error about date not having timezone information + driver.get(self.base_url + "finding/open?last_status_update=2") + def suite(): suite = unittest.TestSuite() @@ -42,6 +47,7 @@ def suite(): suite.addTest(VariousPagesTest("test_finding_group_all_status")) suite.addTest(VariousPagesTest("test_finding_group_closed_status")) suite.addTest(VariousPagesTest("test_finding_group_open_filtered_status")) + suite.addTest(VariousPagesTest("test_date_filter")) return suite From c45aa56fd2671e67f1f1c293be039820e8944274 Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Wed, 17 Sep 2025 22:22:26 +0200 Subject: [PATCH 04/13] NN vulnid (#13180) --- dojo/settings/settings.dist.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index fb2d74e82e6..3b66121d62f 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -1855,6 +1855,7 @@ def saml2_attrib_map_format(din): "MGAA-": "https://advisories.mageia.org/&&.html", # e.g. https://advisories.mageia.org/MGAA-2013-0054.html "MGASA-": "https://advisories.mageia.org/&&.html", # e.g. https://advisories.mageia.org/MGASA-2025-0023.html "NCSC-": "https://advisories.ncsc.nl/advisory?id=", # e.g. https://advisories.ncsc.nl/advisory?id=NCSC-2025-0191 + "NN-": "https://cvepremium.circl.lu/vuln/", # e.g. https://cvepremium.circl.lu/vuln/NN-2021:2-01 "NTAP-": "https://security.netapp.com/advisory/", # e.g. https://security.netapp.com/advisory/ntap-20250328-0007 "OPENSUSE-SU-": "https://osv.dev/vulnerability/", # e.g. https://osv.dev/vulnerability/openSUSE-SU-2025:14898-1 "OSV-": "https://osv.dev/vulnerability/", # e.g. https://osv.dev/vulnerability/OSV-2024-1330 From cb01b7d26768312316ae72f77659103427c50837 Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Fri, 19 Sep 2025 20:10:10 +0200 Subject: [PATCH 05/13] :lipstick: Update Wazuh v4.8 (#13184) * :lipstick: Update Wazuh v4.8 * ruff * update * update * Update dojo/tools/wazuh/v4_8.py Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> --------- Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> --- dojo/tools/wazuh/v4_8.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/dojo/tools/wazuh/v4_8.py b/dojo/tools/wazuh/v4_8.py index 3192e38d231..636ee0210d5 100644 --- a/dojo/tools/wazuh/v4_8.py +++ b/dojo/tools/wazuh/v4_8.py @@ -1,5 +1,3 @@ -import hashlib - from dojo.models import Finding @@ -11,29 +9,26 @@ def parse_findings(self, test, data): item = item_source.get("_source") vuln = item.get("vulnerability") cve = vuln.get("id") + + # Construct a unique key for deduplication + dupe_key = f"{cve}-{item.get('agent', {}).get('id')}" + + if dupe_key in dupes: + continue # Skip if this finding has already been processed + description = vuln.get("description") description += "\nAgent id:" + item.get("agent").get("id") description += "\nAgent name:" + item.get("agent").get("name") severity = vuln.get("severity") cvssv3_score = vuln.get("score").get("base") publish_date = vuln.get("published_at").split("T")[0] - agent_id = item.get("agent").get("id") detection_time = vuln.get("detected_at").split("T")[0] - references = vuln.get("reference") title = ( cve + " affects (version: " + item.get("package").get("version") + ")" ) - dupe_key = title + agent_id + description - dupe_key = hashlib.sha256(dupe_key.encode("utf-8")).hexdigest() - - if dupe_key in dupes: - find = dupes[dupe_key] - else: - dupes[dupe_key] = True - find = Finding( title=title, test=test, @@ -48,6 +43,7 @@ def parse_findings(self, test, data): unique_id_from_tool=dupe_key, date=detection_time, ) - find.unsaved_vulnerability_ids = cve + find.unsaved_vulnerability_ids = [cve] dupes[dupe_key] = find + return list(dupes.values()) From 8a1992c8287fa9c9a46743d451fb0cbd5bb587ab Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Fri, 19 Sep 2025 20:13:06 +0200 Subject: [PATCH 06/13] :tada: Add fix_available information to blackduck component risk #12633 (#13201) --- dojo/tools/blackduck_component_risk/parser.py | 7 ++++++- unittests/tools/test_blackduck_component_risk_parser.py | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/dojo/tools/blackduck_component_risk/parser.py b/dojo/tools/blackduck_component_risk/parser.py index 943966249c2..a7e61490ccf 100644 --- a/dojo/tools/blackduck_component_risk/parser.py +++ b/dojo/tools/blackduck_component_risk/parser.py @@ -67,6 +67,7 @@ def ingest_findings(self, components, securities, sources, test): description = self.license_description(component, source) severity = "High" mitigation = self.license_mitigation(component) + fix_available = bool(mitigation) impact = "N/A" references = self.license_references(component) finding = Finding( @@ -79,6 +80,7 @@ def ingest_findings(self, components, securities, sources, test): references=references, static_finding=True, unique_id_from_tool=component_id, + fix_available=fix_available, ) license_risk.append(finding) elif "None" not in self.license_severity(component): @@ -88,6 +90,7 @@ def ingest_findings(self, components, securities, sources, test): description = self.license_description(component, source) severity = self.license_severity(component) mitigation = self.license_mitigation(component, violation=False) + fix_available = bool(mitigation) impact = "N/A" references = self.license_references(component) finding = Finding( @@ -100,6 +103,7 @@ def ingest_findings(self, components, securities, sources, test): references=references, static_finding=True, unique_id_from_tool=component_id, + fix_available=fix_available, ) license_risk.append(finding) items.extend(license_risk) @@ -111,10 +115,10 @@ def ingest_findings(self, components, securities, sources, test): description = self.security_description(vulns) severity = self.security_severity(vulns) mitigation = self.security_mitigation(vulns) + fix_available = bool(mitigation) impact = self.security_impact(vulns) references = self.security_references(vulns) file_path = self.security_filepath(vulns) - finding = Finding( title=title, test=test, @@ -126,6 +130,7 @@ def ingest_findings(self, components, securities, sources, test): static_finding=True, file_path=file_path, unique_id_from_tool=component_id, + fix_available=fix_available, ) security_risk.append(finding) items.extend(security_risk) diff --git a/unittests/tools/test_blackduck_component_risk_parser.py b/unittests/tools/test_blackduck_component_risk_parser.py index 5ae931bc1f0..773711df64e 100644 --- a/unittests/tools/test_blackduck_component_risk_parser.py +++ b/unittests/tools/test_blackduck_component_risk_parser.py @@ -10,3 +10,11 @@ def test_blackduck_enhanced_zip_upload(self): parser = BlackduckComponentRiskParser() findings = parser.get_findings(testfile, Test()) self.assertEqual(12, len(findings)) + findings = list(findings) + self.assertEqual("License Risk: xmldom:0.1.21", findings[0].title) + self.assertEqual(True, findings[0].fix_available) + self.assertEqual("Package has a license that is In Violation and should not be used: xmldom:0.1.21. Please use another component with an acceptable license.", findings[0].mitigation) + self.assertEqual("High", findings[0].severity) + self.assertEqual("N/A", findings[0].impact) + self.assertEqual("**Project:** foo-project ID-355b2cb252662e07153802b82041e8322ccef144-1.0.0\n", findings[0].references) + self.assertEqual(None, findings[0].file_path) From f75d375a8407ec993f3604cd5c2a4a2d0e658643 Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Fri, 19 Sep 2025 20:37:59 +0200 Subject: [PATCH 07/13] bulk edit: update under_review flag (#13179) * bulk edit: updae under_review flag * bulk edit: updae under_review flag --- dojo/finding/views.py | 1 + dojo/forms.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/dojo/finding/views.py b/dojo/finding/views.py index fad9c2f43f8..7483a47c2ce 100644 --- a/dojo/finding/views.py +++ b/dojo/finding/views.py @@ -2650,6 +2650,7 @@ def finding_bulk_update_all(request, pid=None): find.false_p = form.cleaned_data["false_p"] find.out_of_scope = form.cleaned_data["out_of_scope"] find.is_mitigated = form.cleaned_data["is_mitigated"] + find.under_review = form.cleaned_data["under_review"] find.last_reviewed = timezone.now() find.last_reviewed_by = request.user diff --git a/dojo/forms.py b/dojo/forms.py index 7362cacce95..b9f2b08099f 100644 --- a/dojo/forms.py +++ b/dojo/forms.py @@ -1719,7 +1719,7 @@ def clean_tags(self): class Meta: model = Finding fields = ("severity", "date", "planned_remediation_date", "active", "verified", "false_p", "duplicate", "out_of_scope", - "is_mitigated") + "under_review", "is_mitigated") class EditEndpointForm(forms.ModelForm): From 887d426c64a387867f5b33fcb8ef5bcd5f2c64ee Mon Sep 17 00:00:00 2001 From: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:47:23 -0600 Subject: [PATCH 08/13] Tags: Remove duplicates in edit forms --- dojo/forms.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/dojo/forms.py b/dojo/forms.py index b9f2b08099f..cb2d8054b89 100644 --- a/dojo/forms.py +++ b/dojo/forms.py @@ -342,6 +342,10 @@ 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) @@ -1013,6 +1017,10 @@ 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 @@ -1092,10 +1100,13 @@ class TestForm(forms.ModelForm): attrs={"class": "datepicker", "autocomplete": "off"})) target_end = forms.DateTimeField(widget=forms.TextInput( attrs={"class": "datepicker", "autocomplete": "off"})) - 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 @@ -1452,6 +1463,10 @@ 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", @@ -1723,6 +1738,11 @@ 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"] From 9ca1c58f813c85d0460224b60e1fdd7b3b0189e3 Mon Sep 17 00:00:00 2001 From: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:49:47 -0600 Subject: [PATCH 09/13] Correcting ruff --- dojo/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dojo/forms.py b/dojo/forms.py index cb2d8054b89..2a912bd678c 100644 --- a/dojo/forms.py +++ b/dojo/forms.py @@ -1106,7 +1106,7 @@ class TestForm(forms.ModelForm): 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 From 1d1b84eebf41321640e20fd77b04f23da91cb3f6 Mon Sep 17 00:00:00 2001 From: Paul Osinski <42211303+paulOsinski@users.noreply.github.com> Date: Fri, 19 Sep 2025 20:52:43 -0400 Subject: [PATCH 10/13] [docs] changelog 2.50.2, minor changes (#13226) * add sticker link * update changelog 2.50.2 * update generic findings documentation --------- Co-authored-by: Paul Osinski --- .../content/en/about_defectdojo/about_docs.md | 2 + docs/content/en/changelog/changelog.md | 5 + .../parsers/file/generic.md | 8 +- .../parsers/generic_findings_import.md | 132 +----------------- 4 files changed, 19 insertions(+), 128 deletions(-) diff --git a/docs/content/en/about_defectdojo/about_docs.md b/docs/content/en/about_defectdojo/about_docs.md index e098872c901..49640238f31 100644 --- a/docs/content/en/about_defectdojo/about_docs.md +++ b/docs/content/en/about_defectdojo/about_docs.md @@ -67,6 +67,8 @@ Other guides for working with an Open-Source install: If you run into trouble with an Open Source install, we highly recommend asking questions on the [OWASP Slack](https://owasp.org/slack/invite). Our community members are active on the **# defectdojo** channel and can help you with issues you’re facing. +Looking for cool DefectDojo laptop stickers? As a thank you for being a part of the DefectDojo community, you can sign up to get some free DefectDojo stickers. For more information, check out [this link](https://defectdojo.com/defectdojo-sticker-request). + ### Online Demo A running example of DefectDojo (Open-Source Edition) is available on [our demo server](https://demo.defectdojo.org), using the credentials `admin` / `1Defectdojo@demo#appsec`. The demo server is refreshed regularly and provisioned with some sample data. diff --git a/docs/content/en/changelog/changelog.md b/docs/content/en/changelog/changelog.md index 712dcfa2cb4..2cc71a0a6f7 100644 --- a/docs/content/en/changelog/changelog.md +++ b/docs/content/en/changelog/changelog.md @@ -10,6 +10,11 @@ For Open Source release notes, please see the [Releases page on GitHub](https:// ## Sept 2025: v2.50 +### 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. +* **(Pro UI)** Added Contact Support form for On-Premise installs. + ### Sept 9, 2025: v2.50.1 * **(Tools)** Removed CSV limit for Qualys HackerGuardian diff --git a/docs/content/en/connecting_your_tools/parsers/file/generic.md b/docs/content/en/connecting_your_tools/parsers/file/generic.md index f9c9943378e..6905c0ceed1 100644 --- a/docs/content/en/connecting_your_tools/parsers/file/generic.md +++ b/docs/content/en/connecting_your_tools/parsers/file/generic.md @@ -3,9 +3,9 @@ title: 'Generic Findings Import' toc_hide: true --- -Import Generic findings in CSV or JSON format. +Generic Findings Import can be used to import any report in CSV or JSON format. -Attributes supported for CSV: +### Supported Attributes (CSV) - Date: Date of the finding in mm/dd/yyyy format. - Title: Title of the finding @@ -37,6 +37,8 @@ The CSV expects a header row with the names of the attributes. Date fields are parsed using [dateutil.parse](https://dateutil.readthedocs.io/en/stable/parser.html) supporting a variety of formats such a YYYY-MM-DD or ISO-8601. +### Supported Attributes (JSON) + The list of supported fields in JSON format: - title: **Required.** String @@ -93,7 +95,7 @@ The list of supported fields in JSON format: - ransomware_used: Bool - fix_available: Bool -Example of JSON format: +### Example JSON ```JSON { diff --git a/docs/content/en/connecting_your_tools/parsers/generic_findings_import.md b/docs/content/en/connecting_your_tools/parsers/generic_findings_import.md index a9d40341a30..06c229ef2e1 100644 --- a/docs/content/en/connecting_your_tools/parsers/generic_findings_import.md +++ b/docs/content/en/connecting_your_tools/parsers/generic_findings_import.md @@ -1,136 +1,18 @@ --- -title: "Generic Findings Import" +title: "Using Generic Findings Import" toc_hide: true weight: 2 --- -You can use Generic Findings Import as a method to ingest JSON or CSV files into DefectDojo which are not already in the supported parsers list. +Open-source and Pro users can use Generic Findings Import as a method to ingest JSON or CSV files into DefectDojo which are not already in the supported Tools list. -Files uploaded using Generic Findings Import must conform to the accepted format with respect to CSV column headers / JSON attributes. +Using Generic Findings Import will create a new Test Type in your DefectDojo instance called "`{The Name Of Your Test}` (Generic Findings Import)". For example, this JSON content will result in a Test Type called "Example Report (Generic Findings Import)": -These attributes are supported for CSV: - -- Date: Date of the finding in mm/dd/yyyy format. -- Title: Title of the finding -- CweId: Cwe identifier, must be an integer value. -- epss_score: The probability of exploitation in the next 30 days, must be a float value between 0 and 1.0. -- epss_percentile: The proportion of all scored vulnerabilities with the same or a lower EPSS score, must be a float value between 0 and 1.0. -- Url: Url associated with the finding. -- Severity: Severity of the finding. Must be one of Info, Low, Medium, High, or Critical. -- Description: Description of the finding. Can be multiple lines if enclosed in double quotes. -- Mitigation: Possible Mitigations for the finding. Can be multiple lines if enclosed in double quotes. -- Impact: Detailed impact of the finding. Can be multiple lines if enclosed in double quotes. -- References: References associated with the finding. Can be multiple lines if enclosed in double quotes. -- Active: Indicator if the finding is active. Must be empty, TRUE or FALSE -- Verified: Indicator if the finding has been verified. Must be empty, TRUE, or FALSE -- FalsePositive: Indicator if the finding is a false positive. Must be TRUE, or FALSE. -- Duplicate: Indicator if the finding is a duplicate. Must be TRUE, or FALSE - -The CSV expects a header row with the names of the attributes. - -Example of JSON format: - -```JSON -{ - "findings": [ - { - "title": "test title with endpoints as dict", - "description": "Some very long description with\n\n some UTF-8 chars à qu'il est beau", - "severity": "Medium", - "mitigation": "Some mitigation", - "date": "2021-01-06", - "cve": "CVE-2020-36234", - "cwe": 261, - "cvssv3": "CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N", - "file_path": "src/first.cpp", - "line": 13, - "endpoints": [ - { - "host": "exemple.com" - } - ] - }, - { - "title": "test title with endpoints as strings", - "description": "Some very long description with\n\n some UTF-8 chars à qu'il est beau2", - "severity": "Critical", - "mitigation": "Some mitigation", - "date": "2021-01-06", - "cve": "CVE-2020-36235", - "cwe": 287, - "cvssv3": "CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N", - "file_path": "src/two.cpp", - "line": 135, - "endpoints": [ - "http://urlfiltering.paloaltonetworks.com/test-command-and-control", - "https://urlfiltering.paloaltonetworks.com:2345/test-pest" - ] - }, - { - "title": "test title", - "description": "Some very long description with\n\n some UTF-8 chars à qu'il est beau2", - "severity": "Critical", - "mitigation": "Some mitigation", - "date": "2021-01-06", - "cve": "CVE-2020-36236", - "cwe": 287, - "cvssv3": "CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N", - "file_path": "src/threeeeeeeeee.cpp", - "line": 1353 - } - ] -} -``` - -This parser supports an attributes that accept files as Base64 strings. These files are attached to the respective findings. - -Example: - -```JSON -{ - "name": "My wonderful report", - "findings": [ - { - "title": "Vuln with image", - "description": "Some very long description", - "severity": "Medium", - "files": [ - { - "title": "Screenshot from 2017-04-10 16-54-19.png", - "data": "iVBORw0KGgoAAAANSUhEUgAABWgAAAK0CAIAAAARSkPJAAAAA3N<...>TkSuQmCC" - } - ] - } - ] -} -``` - -This parser supports some additional attributes to be able to define custom `TestTypes` as well as influencing some meta fields on the `Test`: - -- `name`: The internal name of the tool you are using. This is primarily informational, and used for reading the report manually. -- `type`: The name of the test type to create in DefectDojo with the suffix of `(Generic Findings Import)`. The suffix is an important identifier for future users attempting to identify the test type to supply when importing new reports. This value is very important when fetching the correct test type to import findings into, so be sure to keep the `type` consistent from import to import! As an example, a report submitted with a `type` of `Internal Company Tool` will produce a test type in DefectDojo with the title `Internal Company Tool (Generic Findings Import)`. With this newly created test type, you can define custom `HASHCODE_FIELDS` or `DEDUPLICATION_ALGORITHM` in the settings. -- `version`: The version of the tool you are using. This is primarily informational, and is used for reading the report manually and tracking format changes from version to version. -- `description`: A brief description of the test. This could be an explanation of what the tool is reporting, where the tools is maintained, who the point of contact is for the tool when issues arise, or anything in between. -- `static_tool`: Dictates that tool used is running static analysis methods to discover vulnerabilities. -- `dynamic_tool`: Dictates that tool used is running dynamic analysis methods to discover vulnerabilities. -- `soc`: Dictates that tool is used for reporting alerts from a soc (Pro Edition Only). - -Example: - -```JSON { - "name": "My wonderful report", - "type": "My custom Test type", - "version": "1.0.5", - "description": "A unicorn tool that is capable of static analysis, dynamic analysis, and even capturing soc alerts!", - "static_tool": true, - "dynamic_tool": true, - "soc": true, - "findings": [ - ] + "name": "Example Report", + "findings": [] } -``` -### Sample Scan Data +DefectDojo Pro users can also consider using the [Universal Parser](../universal_parser), a tool which allows for highly customizable JSON, XML and CSV imports. -Sample Generic Findings Import scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/generic). \ No newline at end of file +For more information on supported parameters for Generic Findings Import, see the [Parser Guide](../file/generic) \ No newline at end of file From f69c5a063ebc99131440790e0086de80b22b894d Mon Sep 17 00:00:00 2001 From: DefectDojo release bot Date: Mon, 22 Sep 2025 15:15:06 +0000 Subject: [PATCH 11/13] 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..5645dac3561 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "defectdojo", - "version": "2.51.0-dev", + "version": "2.50.3", "license" : "BSD-3-Clause", "private": true, "dependencies": { diff --git a/dojo/__init__.py b/dojo/__init__.py index b094d840779..9430de8cf28 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.2" +__version__ = "2.50.3" __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 41f37583caa..a41da47c4b6 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.3" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.6.208-dev +version: 1.6.208 icon: https://www.defectdojo.org/img/favicon.ico maintainers: - name: madchap From 07fb8ab9b3543af15b5ce26ecb8ae1eb4308b870 Mon Sep 17 00:00:00 2001 From: DefectDojo release bot Date: Mon, 22 Sep 2025 16:01:21 +0000 Subject: [PATCH 12/13] 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 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/dojo/__init__.py b/dojo/__init__.py index 9430de8cf28..09ee079e553 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.51.0-dev" __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 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 bb35fe8645a26f05d84e030e38960dfff5d407f0 Mon Sep 17 00:00:00 2001 From: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:49:16 -0600 Subject: [PATCH 13/13] Update query count in tests --- unittests/test_importers_performance.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/unittests/test_importers_performance.py b/unittests/test_importers_performance.py index a629b7f97c3..5a3291eae45 100644 --- a/unittests/test_importers_performance.py +++ b/unittests/test_importers_performance.py @@ -222,10 +222,10 @@ def test_import_reimport_reimport_performance_no_async_with_product_grading(self self.system_settings(enable_product_grade=True) self.import_reimport_performance( - expected_num_queries1=702, + expected_num_queries1=687, expected_num_async_tasks1=15, - expected_num_queries2=645, + expected_num_queries2=621, expected_num_async_tasks2=28, - expected_num_queries3=322, + expected_num_queries3=302, expected_num_async_tasks3=25, )