From 17f7d6ba3c9ef0abf028b8f1db9f0d291cbde526 Mon Sep 17 00:00:00 2001 From: DefectDojo release bot Date: Tue, 2 Sep 2025 15:00:25 +0000 Subject: [PATCH 01/18] 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 aaf553c9cdf..d96e5e51133 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "defectdojo", - "version": "2.50.0", + "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 868ec8461a2..f2617ac4715 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 -appVersion: "2.50.0" +appVersion: "2.51.0-dev" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.6.205 +version: 1.6.206-dev icon: https://www.defectdojo.org/img/favicon.ico maintainers: - name: madchap From a653695c0f03c034cd22a93cfbce4cd8b9873683 Mon Sep 17 00:00:00 2001 From: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> Date: Tue, 2 Sep 2025 10:42:38 -0600 Subject: [PATCH 02/18] Password Reset: Disable for SSO users (#13079) --- dojo/api_v2/serializers.py | 7 +++++++ dojo/forms.py | 7 +++++++ dojo/user/views.py | 10 +++++----- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index 77e937da85b..1c393c0b066 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -614,6 +614,13 @@ class Meta: model = UserContactInfo fields = "__all__" + def validate(self, data): + user = data.get("user", None) or self.instance.user + if data.get("force_password_reset", False) and not user.has_usable_password(): + msg = "Password resets are not allowed for users authorized through SSO." + raise ValidationError(msg) + return super().validate(data) + class UserStubSerializer(serializers.ModelSerializer): class Meta: diff --git a/dojo/forms.py b/dojo/forms.py index 25b18187469..7362cacce95 100644 --- a/dojo/forms.py +++ b/dojo/forms.py @@ -2388,7 +2388,14 @@ class Meta: exclude = ["user", "slack_user_id"] def __init__(self, *args, **kwargs): + user = kwargs.pop("user", None) super().__init__(*args, **kwargs) + # Do not expose force password reset if the current user does not have a password to reset + if user is not None: + if not user.has_usable_password(): + self.fields["force_password_reset"].disabled = True + self.fields["force_password_reset"].help_text = "This user is authorized through SSO, and does not have a password to reset" + # Determine some other settings based on the current user current_user = get_current_user() if not current_user.is_superuser: if not user_has_configuration_permission(current_user, "auth.change_user") and \ diff --git a/dojo/user/views.py b/dojo/user/views.py index 998dd25a909..3b99eaef004 100644 --- a/dojo/user/views.py +++ b/dojo/user/views.py @@ -230,7 +230,7 @@ def view_profile(request): group_members = get_authorized_group_members_for_user(user) user_contact = user.usercontactinfo if hasattr(user, "usercontactinfo") else None - contact_form = UserContactInfoForm() if user_contact is None else UserContactInfoForm(instance=user_contact) + contact_form = UserContactInfoForm(user=user) if user_contact is None else UserContactInfoForm(instance=user_contact, user=user) global_role = user.global_role if hasattr(user, "global_role") else None if global_role is None: @@ -242,7 +242,7 @@ def view_profile(request): if request.method == "POST": form = DojoUserForm(request.POST, instance=user) - contact_form = UserContactInfoForm(request.POST, instance=user_contact) + contact_form = UserContactInfoForm(request.POST, instance=user_contact, user=user) global_role_form = GlobalRoleForm(request.POST, instance=global_role) if form.is_valid() and contact_form.is_valid() and global_role_form.is_valid(): form.save() @@ -393,7 +393,7 @@ def edit_user(request, uid): form = EditDojoUserForm(instance=user) user_contact = user.usercontactinfo if hasattr(user, "usercontactinfo") else None - contact_form = UserContactInfoForm() if user_contact is None else UserContactInfoForm(instance=user_contact) + contact_form = UserContactInfoForm(user=user) if user_contact is None else UserContactInfoForm(instance=user_contact, user=user) global_role = user.global_role if hasattr(user, "global_role") else None global_role_form = GlobalRoleForm() if global_role is None else GlobalRoleForm(instance=global_role) @@ -401,9 +401,9 @@ def edit_user(request, uid): if request.method == "POST": form = EditDojoUserForm(request.POST, instance=user) if user_contact is None: - contact_form = UserContactInfoForm(request.POST) + contact_form = UserContactInfoForm(request.POST, user=user) else: - contact_form = UserContactInfoForm(request.POST, instance=user_contact) + contact_form = UserContactInfoForm(request.POST, instance=user_contact, user=user) if global_role is None: global_role_form = GlobalRoleForm(request.POST) From 2a252cac8dc9e139edc1b35349e05672dc84990b Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Tue, 2 Sep 2025 18:42:48 +0200 Subject: [PATCH 03/18] :tada: Add fix_available to KrakenDAudit (#13055) * :tada: Add fix_available to KrakenDAudit * Update settings.dist.py --- dojo/tools/krakend_audit/parser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dojo/tools/krakend_audit/parser.py b/dojo/tools/krakend_audit/parser.py index 062c978e3c5..0b1d53af85c 100644 --- a/dojo/tools/krakend_audit/parser.py +++ b/dojo/tools/krakend_audit/parser.py @@ -29,6 +29,7 @@ def get_findings(self, file, test): mitigation=message, static_finding=True, dynamic_finding=False, + fix_available=True, ) findings.append(finding) return findings From e02c26cbbaf78211196be4cdffbd014a0fea4ef4 Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Thu, 4 Sep 2025 04:34:05 +0200 Subject: [PATCH 04/18] :tada: add VAR vulnid (#13096) --- 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 8d6ebee3a35..b71e4e4520c 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -1876,6 +1876,7 @@ def saml2_attrib_map_format(din): "TS-": """https://tailscale.com/security-bulletins#""", # e.g. https://tailscale.com/security-bulletins or https://tailscale.com/security-bulletins#ts-2022-001-1243 "TYPO3-": "https://typo3.org/security/advisory/", # e.g. https://typo3.org/security/advisory/typo3-core-sa-2025-010 "USN-": "https://ubuntu.com/security/notices/", # e.g. https://ubuntu.com/security/notices/USN-6642-1 + "VAR-": "https://cvepremium.circl.lu/vuln/", # e.g. https://cvepremium.circl.lu/vuln/var-201801-0152 "VNS": "https://vulners.com/", "WID-SEC-W-": "https://cvepremium.circl.lu/vuln/", # e.g. https://cvepremium.circl.lu/vuln/wid-sec-w-2025-1468 } From cec647c84ace80d7d52442836d20254f3f0889da Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Thu, 4 Sep 2025 20:44:22 +0200 Subject: [PATCH 05/18] System settings: correct page title/breadcrumbs (#13083) --- dojo/system_settings/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dojo/system_settings/views.py b/dojo/system_settings/views.py index 771e8ff98c1..b0bce9d52d3 100644 --- a/dojo/system_settings/views.py +++ b/dojo/system_settings/views.py @@ -127,7 +127,7 @@ def get( # Set up the initial context context = self.get_context(request) # Add some breadcrumbs - add_breadcrumb(title="Application settings", top_level=False, request=request) + add_breadcrumb(title="System settings", top_level=False, request=request) # Render the page return render(request, self.get_template(), context) @@ -142,6 +142,6 @@ def post( # Check the status of celery request, _ = self.validate_form(request, context) # Add some breadcrumbs - add_breadcrumb(title="Application settings", top_level=False, request=request) + add_breadcrumb(title="System settings", top_level=False, request=request) # Render the page return render(request, self.get_template(), context) From 97d643407e439eb554c2a4057de6a24a9c63198c Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Thu, 4 Sep 2025 21:07:01 +0200 Subject: [PATCH 06/18] :tada: Add number of fix_available information to test view (#13109) * :tada: Add number of fix_available information to test view * Update dojo/templates/dojo/view_test.html Co-authored-by: valentijnscholten --------- Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> Co-authored-by: valentijnscholten --- dojo/templates/dojo/view_test.html | 2 ++ dojo/test/views.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/dojo/templates/dojo/view_test.html b/dojo/templates/dojo/view_test.html index 3114e19d2bf..824cad760b7 100644 --- a/dojo/templates/dojo/view_test.html +++ b/dojo/templates/dojo/view_test.html @@ -108,6 +108,7 @@

{% trans "Dates" %} {% trans "Updated" %} {% if test.percent_complete > 0 %}{% trans "Progress" %}{% endif %} + {% trans "Fix Available" %} {% trans "Version" %} {% if 'TRACK_IMPORT_HISTORY'|setting_enabled and test.test_import_set.all %} @@ -158,6 +159,7 @@

{% endif %} + {{ fix_available_count }} {{ test.version }} {% if 'TRACK_IMPORT_HISTORY'|setting_enabled and test.total_reimport_count %} {{ test.total_reimport_count }} diff --git a/dojo/test/views.py b/dojo/test/views.py index 519b45ae8c7..46af27b444a 100644 --- a/dojo/test/views.py +++ b/dojo/test/views.py @@ -120,10 +120,12 @@ def get_findings(self, request: HttpRequest, test: Test): finding_filter_class = FindingFilterWithoutObjectLookups if filter_string_matching else FindingFilter findings = finding_filter_class(request.GET, pid=test.engagement.product.id, queryset=findings) paged_findings = get_page_items_and_count(request, prefetch_for_findings(findings.qs), 25, prefix="findings") + fix_available_count = findings.qs.filter(fix_available=True).count() return { "findings": paged_findings, "filtered": findings, + "fix_available_count": fix_available_count, } def get_note_form(self, request: HttpRequest): From 44cbfecc25e8dc9c5a6604e90f2489212414aec2 Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Thu, 4 Sep 2025 21:07:55 +0200 Subject: [PATCH 07/18] :tada: Add fix_available information to aqua parser #12633 (#13106) --- dojo/tools/aqua/parser.py | 12 ++++++++++-- unittests/tools/test_aqua_parser.py | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/dojo/tools/aqua/parser.py b/dojo/tools/aqua/parser.py index 01e697f9224..2ae981f4e8b 100644 --- a/dojo/tools/aqua/parser.py +++ b/dojo/tools/aqua/parser.py @@ -147,7 +147,10 @@ def get_item(self, resource, vuln, test): resource_name = resource.get("name", resource.get("path")) resource_version = resource.get("version", "No version") vulnerability_id = vuln.get("name", "No CVE") - fix_version = vuln.get("fix_version", "None") + fix_available = False + fix_version = vuln.get("fix_version", None) + if fix_version is not None: + fix_available = True description = vuln.get("description", "No description.") + "\n" if resource.get("path"): description += "**Path:** " + resource.get("path") + "\n" @@ -222,6 +225,7 @@ def get_item(self, resource, vuln, test): component_name=resource.get("name"), component_version=resource.get("version"), impact=severity, + fix_available=fix_available, ) cvss_data = parse_cvss_data(cvssv3) @@ -244,7 +248,10 @@ def get_item_v2(self, item, test): severity = self.severity_of(float(item["score"])) description = item.get("description") solution = item.get("solution") - fix_version = item.get("fix_version") + fix_available = False + fix_version = item.get("fix_version", None) + if fix_version is not None: + fix_available = True if solution: mitigation = solution elif fix_version: @@ -260,6 +267,7 @@ def get_item_v2(self, item, test): severity=severity, impact=severity, mitigation=mitigation, + fix_available=fix_available, ) finding.unsaved_vulnerability_ids = [vulnerability_id] return finding diff --git a/unittests/tools/test_aqua_parser.py b/unittests/tools/test_aqua_parser.py index d1278ff2886..d7141aa9475 100644 --- a/unittests/tools/test_aqua_parser.py +++ b/unittests/tools/test_aqua_parser.py @@ -23,6 +23,7 @@ def test_aqua_parser_has_one_finding(self): self.assertEqual("CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", finding.cvssv3) self.assertEqual("musl libc through 1.1.23 has an x87 floating-point stack adjustment imbalance, related to the math/i386/ directory. In some cases, use of this library could introduce out-of-bounds writes that are not present in an application's source code.", finding.description) self.assertEqual("1.1.20-r5", finding.mitigation) + self.assertEqual(True, finding.fix_available) self.assertEqual("\nhttps://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-14697", finding.references) self.assertEqual("musl", finding.component_name) self.assertEqual("1.1.20-r4", finding.component_version) @@ -55,6 +56,7 @@ def test_aqua_parser_v2_has_one_finding(self): self.assertEqual("Medium", finding.severity) self.assertEqual("CURL before 7.68.0 lacks proper input validation, which allows users to create a `FILE:` URL that can make the client access a remote file using SMB (Windows-only issue).", finding.description) self.assertEqual("Upgrade to curl 7.68.0", finding.mitigation) + self.assertEqual(True, finding.fix_available) self.assertEqual(1, len(finding.unsaved_vulnerability_ids)) self.assertEqual("CVE-2019-15601", finding.unsaved_vulnerability_ids[0]) From de1f539ccc5cddeb006eb0077d6a93c0df385d6c Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Thu, 4 Sep 2025 21:45:48 +0200 Subject: [PATCH 08/18] uwsgi: default to 4 processes x 4 threads (#13080) --- Dockerfile.django-alpine | 4 +- Dockerfile.django-debian | 4 +- docker/entrypoint-uwsgi-dev.sh | 4 +- docker/entrypoint-uwsgi.sh | 4 +- .../installation/running-in-production.md | 4 +- docs/content/en/open_source/performance.md | 39 ------------------- helm/defectdojo/templates/configmap.yaml | 4 +- 7 files changed, 12 insertions(+), 51 deletions(-) delete mode 100644 docs/content/en/open_source/performance.md diff --git a/Dockerfile.django-alpine b/Dockerfile.django-alpine index 9d3b401975b..79731e55d0d 100644 --- a/Dockerfile.django-alpine +++ b/Dockerfile.django-alpine @@ -132,8 +132,8 @@ ENV \ DD_INITIALIZE=true \ DD_UWSGI_MODE="socket" \ DD_UWSGI_ENDPOINT="0.0.0.0:3031" \ - DD_UWSGI_NUM_OF_PROCESSES="2" \ - DD_UWSGI_NUM_OF_THREADS="2" + DD_UWSGI_NUM_OF_PROCESSES="4" \ + DD_UWSGI_NUM_OF_THREADS="4" ENTRYPOINT ["/entrypoint-uwsgi.sh"] FROM django AS django-unittests diff --git a/Dockerfile.django-debian b/Dockerfile.django-debian index da5b4b3a22a..2a263e1bf48 100644 --- a/Dockerfile.django-debian +++ b/Dockerfile.django-debian @@ -135,8 +135,8 @@ ENV \ DD_INITIALIZE=true \ DD_UWSGI_MODE="socket" \ DD_UWSGI_ENDPOINT="0.0.0.0:3031" \ - DD_UWSGI_NUM_OF_PROCESSES="2" \ - DD_UWSGI_NUM_OF_THREADS="2" + DD_UWSGI_NUM_OF_PROCESSES="4" \ + DD_UWSGI_NUM_OF_THREADS="4" ENTRYPOINT ["/entrypoint-uwsgi.sh"] FROM django AS django-unittests diff --git a/docker/entrypoint-uwsgi-dev.sh b/docker/entrypoint-uwsgi-dev.sh index 200e3ec2107..45b6204f5a9 100755 --- a/docker/entrypoint-uwsgi-dev.sh +++ b/docker/entrypoint-uwsgi-dev.sh @@ -34,8 +34,8 @@ exec uwsgi \ --protocol uwsgi \ --wsgi dojo.wsgi:application \ --enable-threads \ - --processes "${DD_UWSGI_NUM_OF_PROCESSES:-2}" \ - --threads "${DD_UWSGI_NUM_OF_THREADS:-2}" \ + --processes "${DD_UWSGI_NUM_OF_PROCESSES:-4}" \ + --threads "${DD_UWSGI_NUM_OF_THREADS:-4}" \ --reload-mercy 1 \ --worker-reload-mercy 1 \ --py-autoreload 1 \ diff --git a/docker/entrypoint-uwsgi.sh b/docker/entrypoint-uwsgi.sh index a355ceeb7ee..f15f2b49958 100755 --- a/docker/entrypoint-uwsgi.sh +++ b/docker/entrypoint-uwsgi.sh @@ -36,8 +36,8 @@ exec uwsgi \ "--${DD_UWSGI_MODE}" "${DD_UWSGI_ENDPOINT}" \ --protocol uwsgi \ --enable-threads \ - --processes "${DD_UWSGI_NUM_OF_PROCESSES:-2}" \ - --threads "${DD_UWSGI_NUM_OF_THREADS:-2}" \ + --processes "${DD_UWSGI_NUM_OF_PROCESSES:-4}" \ + --threads "${DD_UWSGI_NUM_OF_THREADS:-4}" \ --wsgi dojo.wsgi:application \ --buffer-size="${DD_UWSGI_BUFFER_SIZE:-8192}" \ --http 0.0.0.0:8081 --http-to "${DD_UWSGI_ENDPOINT}" \ diff --git a/docs/content/en/open_source/installation/running-in-production.md b/docs/content/en/open_source/installation/running-in-production.md index bee0830b204..e060d1e212c 100644 --- a/docs/content/en/open_source/installation/running-in-production.md +++ b/docs/content/en/open_source/installation/running-in-production.md @@ -58,9 +58,9 @@ handle 4 concurrent connections. Based on your resource settings, you can tweak: - `DD_UWSGI_NUM_OF_PROCESSES` for the number of spawned processes. - (default 2) + (default 4) - `DD_UWSGI_NUM_OF_THREADS` for the number of threads in these - processes. (default 2) + processes. (default 4) For example, you may have 4 processes with 6 threads each, yielding 24 concurrent connections. diff --git a/docs/content/en/open_source/performance.md b/docs/content/en/open_source/performance.md deleted file mode 100644 index 4504e4a3158..00000000000 --- a/docs/content/en/open_source/performance.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: "Performance Enhancements (Open Source)" -description: "Settings to configure to enhance performance in DefectDojo" -draft: false -weight: 4 ---- - -## Filter String Matching Optimization - -IN the UI, many of the filters for a given object will also query related objects -for an easy visual match of an item to filter on. For instances with many objects, -this could lead to a considerable performance hit. To alleviate this constriction, -enable the "Filter String Matching Optimization" setting in the System Settings to -change many filters to only search on names, rather than the objects themselves. -This change will save many large queries, and will improve the performance of UI -based interactions. - -## Asynchronous Delete - -For larger instances, deleting an object can take minutes for all related objects to be -expanded into memory, rendered on the page, and then removing all objects from the database. -To combat this issue, two settings can be set in `local_settings.py`: - -#### ASYNC_OBJECT_DELETE - -Deleting an object asynchronously changes the way an object is deleted under the hood. By removing -the need to expand into memory, a lot of time (and memory) can be saved by offloading the lookups and -removals onto celery processes. This process works by starting at the bottom of a given object, and -walking the tree upwards rather than downwards. This way, objects can be seperated into buckets, -and then deleted. - -#### DELETE_PREVIEW - -Previewing all the objects to be deleted takes almost as much time as deleting the objects itself. -This is a safety feature intended to warn users of what they are about to delete, as well as educating -users of how the delete functionality works by cascade deleting all related objects. With this feature enabled, -the user will only see the following text in the delete preview (without any database lookups) - -`Previewing the relationships has been disabled.` diff --git a/helm/defectdojo/templates/configmap.yaml b/helm/defectdojo/templates/configmap.yaml index 74d516981c7..a5f3706526e 100644 --- a/helm/defectdojo/templates/configmap.yaml +++ b/helm/defectdojo/templates/configmap.yaml @@ -45,8 +45,8 @@ data: DD_UWSGI_ENDPOINT: /run/defectdojo/uwsgi.sock DD_UWSGI_HOST: localhost DD_UWSGI_PASS: unix:///run/defectdojo/uwsgi.sock - DD_UWSGI_NUM_OF_PROCESSES: '{{ .Values.django.uwsgi.appSettings.processes | default 2 }}' - DD_UWSGI_NUM_OF_THREADS: '{{ .Values.django.uwsgi.appSettings.threads | default 2 }}' + DD_UWSGI_NUM_OF_PROCESSES: '{{ .Values.django.uwsgi.appSettings.processes | default 4 }}' + DD_UWSGI_NUM_OF_THREADS: '{{ .Values.django.uwsgi.appSettings.threads | default 4 }}' DD_UWSGI_MAX_FD: '{{ .Values.django.uwsgi.appSettings.maxFd }}' DD_DJANGO_METRICS_ENABLED: '{{ .Values.monitoring.enabled }}' NGINX_METRICS_ENABLED: '{{ .Values.monitoring.enabled }}' From 45030d52a610e66b4ca03807136e2beb5b66adcf Mon Sep 17 00:00:00 2001 From: Jino Tesauro <53376807+Jino-T@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:17:23 -0500 Subject: [PATCH 09/18] allow qualys hacker guardian parser to parse larger csv files (#13120) Co-authored-by: Jino Tesauro --- dojo/tools/qualys_hacker_guardian/parser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dojo/tools/qualys_hacker_guardian/parser.py b/dojo/tools/qualys_hacker_guardian/parser.py index cba4e74d582..1c1992dfe4c 100644 --- a/dojo/tools/qualys_hacker_guardian/parser.py +++ b/dojo/tools/qualys_hacker_guardian/parser.py @@ -1,5 +1,6 @@ import csv import io +import sys from dateutil import parser as date_parser @@ -88,6 +89,7 @@ def get_findings(self, filename, test): content = filename.read() if isinstance(content, bytes): content = content.decode("utf-8") + csv.field_size_limit(int(sys.maxsize / 10)) reader = csv.DictReader(io.StringIO(content), delimiter=",", quotechar='"') dupes = {} for row in reader: From 8ec84b48c10b1acf1ba06c652c62e6c7efe202e1 Mon Sep 17 00:00:00 2001 From: Paul Osinski <42211303+paulOsinski@users.noreply.github.com> Date: Fri, 5 Sep 2025 14:43:35 -0400 Subject: [PATCH 10/18] Update changelog 2.50 (#13121) * add about_deduplication png * update changelog 2.50 * update changelog 2.50 --------- Co-authored-by: Paul Osinski --- docs/content/en/changelog/changelog.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/content/en/changelog/changelog.md b/docs/content/en/changelog/changelog.md index b0ce860a70a..03bffdbac42 100644 --- a/docs/content/en/changelog/changelog.md +++ b/docs/content/en/changelog/changelog.md @@ -8,6 +8,14 @@ Here are the release notes for **DefectDojo Pro (Cloud Version)**. These release For Open Source release notes, please see the [Releases page on GitHub](https://github.com/DefectDojo/django-DefectDojo/releases), or alternatively consult the Open Source [upgrade notes](/en/open_source/upgrading/upgrading_guide/). +## Sept 2025: v2.50 + +### Sept 2, 2025: v2.50.0 + +* **(Pro UI)** "Date During" filter has been added to the UI, allowing users to filter by a range of dates +* **(Pro UI)** Vulnerability ID column can now be sorted, however the sorting only considers the **first** vulnerability ID. +* **(Pro UI)** Request/Response pairs can now be added / updated / deleted via the Edit Finding form. + ## August 2025: v2.49 The Pro UI has been significantly reorganized, with changes to page organization. From 7eee8181aec40851135fa0e5eeb721b1f42c3dc1 Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Fri, 5 Sep 2025 20:45:11 +0200 Subject: [PATCH 11/18] semgrep pro parser (#12848) * semgrep pro: parse sast finding * update docs --- .../parsers/file/semgrep_pro.md | 60 ++++++ dojo/tools/semgrep_pro/__init__.py | 0 dojo/tools/semgrep_pro/parser.py | 195 ++++++++++++++++++ unittests/scans/semgrep_pro/no_vuln.json | 4 + unittests/scans/semgrep_pro/one_vuln.json | 95 +++++++++ unittests/tools/test_semgrep_pro_parser.py | 73 +++++++ 6 files changed, 427 insertions(+) create mode 100644 docs/content/en/connecting_your_tools/parsers/file/semgrep_pro.md create mode 100644 dojo/tools/semgrep_pro/__init__.py create mode 100644 dojo/tools/semgrep_pro/parser.py create mode 100644 unittests/scans/semgrep_pro/no_vuln.json create mode 100644 unittests/scans/semgrep_pro/one_vuln.json create mode 100644 unittests/tools/test_semgrep_pro_parser.py diff --git a/docs/content/en/connecting_your_tools/parsers/file/semgrep_pro.md b/docs/content/en/connecting_your_tools/parsers/file/semgrep_pro.md new file mode 100644 index 00000000000..e2bb635dacc --- /dev/null +++ b/docs/content/en/connecting_your_tools/parsers/file/semgrep_pro.md @@ -0,0 +1,60 @@ +--- +title: "Semgrep Pro JSON Report" +toc_hide: true +--- +Import Semgrep Pro findings in JSON format. + +### Sample Scan Data +Sample Semgrep Pro JSON Report scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/semgrep_pro). + +### Default Deduplication +By default, DefectDojo uses the `match_based_id` from Semgrep Pro for deduplication. If this is not available, it falls back to using a combination of: +- title +- file path +- line number + +### Fields Mapped +The following fields are mapped from the Semgrep Pro JSON report: + +#### Basic Information +- title: Mapped from `rule_name` +- severity: Mapped from Semgrep Pro severity levels (ERROR/HIGH → High, WARNING/MEDIUM → Medium, INFO/LOW → Low) +- file_path: Path to the affected file from `location.file_path` +- line: Line number from `location.line` +- unique_id_from_tool: Mapped from `match_based_id` + +#### Status Fields +- active: Set to false if status is "fixed" or "removed" +- verified: Set to true if triage_state is not "untriaged" + +#### Rich Content Fields +- description: Includes: + - Rule message and details + - CWE references + - OWASP references + - Categories + - Triage information +- impact: Includes: + - Vulnerability classes + - Confidence level + - Repository information +- mitigation: Includes: + - Guidance summary + - Detailed instructions + - Auto-fix suggestions + - Auto-triage information + - Component details and risk level +- references: Includes: + - Line of code URL + - CWE references + - OWASP references + - External ticket information + +#### Component Information +- component_name: Mapped from `assistant.component.tag` + +#### Additional Fields +- static_finding: Always set to true +- dynamic_finding: Always set to false +- cwe: Extracted from first CWE reference if available +- date: Mapped from `created_at` \ No newline at end of file diff --git a/dojo/tools/semgrep_pro/__init__.py b/dojo/tools/semgrep_pro/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dojo/tools/semgrep_pro/parser.py b/dojo/tools/semgrep_pro/parser.py new file mode 100644 index 00000000000..d4371073f5b --- /dev/null +++ b/dojo/tools/semgrep_pro/parser.py @@ -0,0 +1,195 @@ +import contextlib +import json +from datetime import datetime + +from dojo.models import Finding + + +class SemgrepProParser: + def get_scan_types(self): + return ["Semgrep Pro JSON Report"] + + def get_label_for_scan_types(self, scan_type): + return scan_type + + def get_description_for_scan_types(self, scan_type): + return "Import Semgrep Pro findings in JSON format" + + def get_findings(self, filename, test): + data = json.load(filename) + dupes = {} + + for item in data.get("findings", []): + # Ensure required fields have default values + title = item.get("rule_name", "No title") + file_path = item.get("location", {}).get("file_path", "") + line = item.get("location", {}).get("line", 0) + + # Map status to active/verified + status = item.get("status", "new").lower() + active = status not in {"fixed", "removed"} + triage_status = item.get("triage_state", "untriaged").lower() + verified = triage_status != "untriaged" + + finding = Finding( + test=test, + title=title, + severity=self.convert_severity(item.get("severity", "INFO")), + description=self.get_description(item), + file_path=file_path, + line=line, + static_finding=True, + dynamic_finding=False, + vuln_id_from_tool=item.get("rule_name"), + nb_occurences=1, + active=active, + verified=verified, + ) + + # Add CWE if available + if "rule" in item and "cwe_names" in item["rule"]: + try: + cwe_name = item["rule"]["cwe_names"][0] # Take first CWE + finding.cwe = int(cwe_name.split("-")[1].split(":")[0]) + except (ValueError, IndexError, KeyError): + finding.cwe = None + + # Add references if available + references = [] + if "line_of_code_url" in item: + references.append(f"Line of Code: {item['line_of_code_url']}") + if "rule" in item: + if "owasp_names" in item["rule"]: + references.extend(item["rule"]["owasp_names"]) + if "cwe_names" in item["rule"]: + references.extend(item["rule"]["cwe_names"]) + + if "external_ticket" in item: + references.append(f"External Ticket: \n {item['external_ticket']}") + + # Add file location details + if references: + finding.references = "\n".join(references) + + # Add mitigation if available + mitigation_parts = [] + if "assistant" in item: + assistant = item["assistant"] + if "guidance" in assistant: + if "summary" in assistant["guidance"]: + mitigation_parts.append(f"**Guidance Summary:**\n{assistant['guidance']['summary']}") + if "instructions" in assistant["guidance"]: + mitigation_parts.append(f"**Instructions:**\n{assistant['guidance']['instructions']}") + + if "autofix" in assistant: + autofix = assistant["autofix"] + if "fix_code" in autofix: + mitigation_parts.append(f"**Suggested Fix:**\n```\n{autofix['fix_code']}\n```") + if autofix.get("explanation"): + mitigation_parts.append(f"**Fix Explanation:**\n{autofix['explanation']}") + + if "autotriage" in assistant: + autotriage = assistant["autotriage"] + if "verdict" in autotriage: + mitigation_parts.append(f"**Auto-triage Verdict:** {autotriage['verdict']}") + if "reason" in autotriage: + mitigation_parts.append(f"**Auto-triage Reason:** {autotriage['reason']}") + + if "component" in assistant: + component = assistant["component"] + if "tag" in component: + mitigation_parts.append(f"**Component:** {component['tag']}") + if "risk" in component: + mitigation_parts.append(f"**Risk Level:** {component['risk']}") + + finding.mitigation = "\n\n".join(mitigation_parts) if mitigation_parts else None + + # Add unique identifier + finding.unique_id_from_tool = item.get("match_based_id") + + # Add component name and version if available + if "assistant" in item and "component" in item["assistant"]: + finding.component_name = item["assistant"]["component"].get("tag") + + # Add dates + if "created_at" in item: + with contextlib.suppress(ValueError, TypeError): + finding.date = datetime.strptime(item["created_at"].split(".")[0], "%Y-%m-%dT%H:%M:%S") + + # Add impact + impact_parts = [] + if "rule" in item and "vulnerability_classes" in item["rule"]: + impact_parts.extend(item["rule"]["vulnerability_classes"]) + if "confidence" in item: + impact_parts.append(f"Confidence: {item['confidence'].capitalize()}") + if "repository" in item: + repo = item["repository"] + impact_parts.append(f"Repository: {repo.get('name', '')} ({repo.get('url', '')})") + finding.impact = "\n".join(impact_parts) + + # Use match_based_id for deduplication if available, otherwise use file location + dupe_key = finding.unique_id_from_tool or title + str(file_path) + str(line) + + if dupe_key in dupes: + dupes[dupe_key].nb_occurences += 1 + else: + dupes[dupe_key] = finding + + return list(dupes.values()) + + def convert_severity(self, val): + val = val.upper() + if val == "ERROR" or val == "HIGH": + return "High" + if val == "WARNING" or val == "MEDIUM": + return "Medium" + if val == "INFO" or val == "LOW": + return "Low" + if val == "CRITICAL": + return "Critical" + return "Info" + + def get_description(self, item): + desc = "" + if "rule_message" in item: + desc += f"**Message:** {item['rule_message']}\n\n" + + if "rule" in item: + if "message" in item["rule"]: + desc += f"**Rule Message:** {item['rule']['message']}\n\n" + if "category" in item["rule"]: + desc += f"**Category:** {item['rule']['category']}\n\n" + if "confidence" in item["rule"]: + desc += f"**Confidence:** {item['rule']['confidence']}\n\n" + if "vulnerability_classes" in item["rule"]: + desc += "**Vulnerability Classes:**\n" + for vuln_class in item["rule"]["vulnerability_classes"]: + desc += f"- {vuln_class}\n" + desc += "\n" + if "cwe_names" in item["rule"]: + desc += "**CWE References:**\n" + for cwe in item["rule"]["cwe_names"]: + desc += f"- {cwe}\n" + desc += "\n" + if "owasp_names" in item["rule"]: + desc += "**OWASP References:**\n" + for owasp in item["rule"]["owasp_names"]: + desc += f"- {owasp}\n" + desc += "\n" + + # Add categories + if "categories" in item: + desc += "**Categories:**\n" + for category in item["categories"]: + desc += f"- {category}\n" + desc += "\n" + + # Add triage information + if "triage_state" in item: + desc += f"**Triage State:** {item['triage_state']}\n" + if "triage_comment" in item: + desc += f"**Triage Comment:** {item['triage_comment']}\n" + if "triage_reason" in item: + desc += f"**Triage Reason:** {item['triage_reason']}\n\n" + + return desc diff --git a/unittests/scans/semgrep_pro/no_vuln.json b/unittests/scans/semgrep_pro/no_vuln.json new file mode 100644 index 00000000000..5865a185a47 --- /dev/null +++ b/unittests/scans/semgrep_pro/no_vuln.json @@ -0,0 +1,4 @@ +{ + "findings": [ + ] +} \ No newline at end of file diff --git a/unittests/scans/semgrep_pro/one_vuln.json b/unittests/scans/semgrep_pro/one_vuln.json new file mode 100644 index 00000000000..d3eb030f81c --- /dev/null +++ b/unittests/scans/semgrep_pro/one_vuln.json @@ -0,0 +1,95 @@ +{ + "findings": [ + { + "id": 1234567, + "ref": "refs/pull/1234/merge", + "first_seen_scan_id": 1234, + "syntactic_id": "440eeface888e78afceac3dc7d4cc2cf", + "match_based_id": "0f8c79a6f7e0ff2f908ff5bc366ae1548465069bae8892088051e1c3b4b12c6b8df37d5bcbb181eb868aa79f81f239d14bf2336d552786ab8ccdc7279adf07a6_1", + "external_ticket": { + "external_slug": "OPS-158", + "url": "string", + "id": 0, + "linked_issue_ids": [ + 0 + ] + }, + "review_comments": [ + { + "external_discussion_id": "af04762b69acfb74c8f9", + "external_note_id": 123523 + } + ], + "repository": { + "name": "semgrep", + "url": "https://github.com/semgrep/semgrep" + }, + "line_of_code_url": "https://github.com/semgrep/semgrep/blob/39f95450a7d4d70e54c9edbd109bed8210a36889/src/core_cli/Core_CLI.ml#L1", + "triage_state": "untriaged", + "state": "unresolved", + "status": "open", + "severity": "medium", + "confidence": "medium", + "categories": [ + "security" + ], + "created_at": "2020-11-18T23:28:12.391807Z", + "relevant_since": "2020-11-18T23:28:12.391807Z", + "rule_name": "typescript.react.security.audit.react-no-refs.react-no-refs", + "rule_message": "`ref` usage found. refs give direct DOM access and may create a possibility for XSS, which could cause\nsensitive information such as user cookies to be retrieved by an attacker. Instead, avoid direct DOM\nmanipulation or use DOMPurify to sanitize HTML before writing it into the page.\n", + "location": { + "file_path": "frontend/src/corpComponents/Code.tsx", + "line": 120, + "column": 8, + "end_line": 124, + "end_column": 16 + }, + "sourcing_policy": { + "id": 120, + "name": "Default Policy", + "slug": "default-policy" + }, + "triaged_at": "2020-11-19T23:28:12.391807Z", + "triage_comment": "This finding is from the test repo", + "triage_reason": "acceptable_risk", + "state_updated_at": "2020-11-19T23:28:12.391807Z", + "rule": { + "name": "html.security.plaintext-http-link.plaintext-http-link", + "message": "This link points to a plaintext HTTP URL. Prefer an encrypted HTTPS URL if possible.", + "confidence": "high", + "category": "security", + "subcategories": [ + "vuln" + ], + "vulnerability_classes": [ + "Mishandled Sensitive Information" + ], + "cwe_names": [ + "CWE-319: Cleartext Transmission of Sensitive Information" + ], + "owasp_names": [ + "A03:2017 - Sensitive Data Exposure", + "A02:2021 - Cryptographic Failures" + ] + }, + "assistant": { + "autofix": { + "fix_code": "cookie.setHttpOnly(true);\nresponse.addCookie(cookie);", + "explanation": "" + }, + "guidance": { + "summary": "Use a template rendering engine such as EJS instead of string concatenation.", + "instructions": "1. Check if your project has any template engines installed such as EJS, Pug, or Mustache.\n If not, install EJS, with a command such as `$ npm install ejs`.\n2. Create an EJS template: `const template = '

<%= user.id %>

'`\n3. <... example trimmed in API docs ...>" + }, + "autotriage": { + "verdict": "false_positive", + "reason": "The matched code is used for a non-security related feature." + }, + "component": { + "tag": "user data", + "risk": "high" + } + } + } + ] +} \ No newline at end of file diff --git a/unittests/tools/test_semgrep_pro_parser.py b/unittests/tools/test_semgrep_pro_parser.py new file mode 100644 index 00000000000..2b40ff32c4c --- /dev/null +++ b/unittests/tools/test_semgrep_pro_parser.py @@ -0,0 +1,73 @@ +from django.test import TestCase + +from dojo.models import Test +from dojo.tools.semgrep_pro.parser import SemgrepProParser +from unittests.dojo_test_case import get_unit_tests_scans_path + + +class TestSemgrepProParser(TestCase): + def test_parse_no_findings(self): + path = get_unit_tests_scans_path("semgrep_pro") / "no_vuln.json" + with path.open(encoding="utf-8") as testfile: + parser = SemgrepProParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(0, len(findings)) + + def test_parse_one_finding(self): + path = get_unit_tests_scans_path("semgrep_pro") / "one_vuln.json" + with path.open(encoding="utf-8") as testfile: + parser = SemgrepProParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(1, len(findings)) + + finding = findings[0] + # Basic fields + self.assertEqual("Medium", finding.severity) + self.assertEqual("typescript.react.security.audit.react-no-refs.react-no-refs", finding.vuln_id_from_tool) + self.assertEqual("frontend/src/corpComponents/Code.tsx", finding.file_path) + self.assertEqual(120, finding.line) + self.assertEqual(319, finding.cwe) # CWE-319: Cleartext Transmission of Sensitive Information + self.assertTrue(finding.static_finding) + self.assertFalse(finding.dynamic_finding) + + # Status fields + self.assertTrue(finding.active) # status is "open" + self.assertFalse(finding.verified) # triage_state is "untriaged" + + # Description field - check for key components + self.assertIsNotNone(finding.description) + self.assertIn("Message:", finding.description) + self.assertIn("Rule Message:", finding.description) + self.assertIn("CWE References:", finding.description) + self.assertIn("CWE-319:", finding.description) + self.assertIn("OWASP References:", finding.description) + self.assertIn("A03:2017", finding.description) + self.assertIn("A02:2021", finding.description) + self.assertIn("Categories:", finding.description) + self.assertIn("security", finding.description) + + # Impact field - check for key components + self.assertIsNotNone(finding.impact) + self.assertIn("Mishandled Sensitive Information", finding.impact) + self.assertIn("Confidence: Medium", finding.impact) + self.assertIn("Repository: semgrep", finding.impact) + + # Mitigation field - check for key components + self.assertIsNotNone(finding.mitigation) + self.assertIn("**Guidance Summary:**", finding.mitigation) + self.assertIn("template rendering engine", finding.mitigation) + self.assertIn("**Instructions:**", finding.mitigation) + self.assertIn("npm install ejs", finding.mitigation) + self.assertIn("**Auto-triage Verdict:** false_positive", finding.mitigation) + self.assertIn("**Component:** user data", finding.mitigation) + self.assertIn("**Risk Level:** high", finding.mitigation) + + # References field - check for key components + self.assertIsNotNone(finding.references) + self.assertIn("Line of Code:", finding.references) + self.assertIn("CWE-319:", finding.references) + self.assertIn("A03:2017", finding.references) + self.assertIn("External Ticket:", finding.references) + + # Unique identifier + self.assertEqual("0f8c79a6f7e0ff2f908ff5bc366ae1548465069bae8892088051e1c3b4b12c6b8df37d5bcbb181eb868aa79f81f239d14bf2336d552786ab8ccdc7279adf07a6_1", finding.unique_id_from_tool) From 8e949dad8d0313781a2903d314726611a7dec3b7 Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Fri, 5 Sep 2025 20:46:49 +0200 Subject: [PATCH 12/18] :bug: Implement Wazuh v4.8 (#12739) * :bug: Implement Wazuh v4.8 * update unittests * update * fix * fix * fix * update unittests * update * fix unittest * review --- dojo/tools/wazuh/parser.py | 76 +-- dojo/tools/wazuh/v4_7.py | 70 ++ dojo/tools/wazuh/v4_8.py | 53 ++ ..._findings.json => v4-7_many_findings.json} | 0 ...no_findings.json => v4-7_no_findings.json} | 0 ...one_finding.json => v4-7_one_finding.json} | 0 ...on => v4-7_one_finding_with_endpoint.json} | 0 unittests/scans/wazuh/v4-8_many_findings.json | 629 ++++++++++++++++++ unittests/tools/test_wazuh_parser.py | 25 +- 9 files changed, 776 insertions(+), 77 deletions(-) create mode 100644 dojo/tools/wazuh/v4_7.py create mode 100644 dojo/tools/wazuh/v4_8.py rename unittests/scans/wazuh/{many_findings.json => v4-7_many_findings.json} (100%) rename unittests/scans/wazuh/{no_findings.json => v4-7_no_findings.json} (100%) rename unittests/scans/wazuh/{one_finding.json => v4-7_one_finding.json} (100%) rename unittests/scans/wazuh/{one_finding_with_endpoint.json => v4-7_one_finding_with_endpoint.json} (100%) create mode 100644 unittests/scans/wazuh/v4-8_many_findings.json diff --git a/dojo/tools/wazuh/parser.py b/dojo/tools/wazuh/parser.py index c7982733e30..60c3e447ca0 100644 --- a/dojo/tools/wazuh/parser.py +++ b/dojo/tools/wazuh/parser.py @@ -1,7 +1,7 @@ -import hashlib import json -from dojo.models import Endpoint, Finding +from dojo.tools.wazuh.v4_7 import WazuhV4_7 +from dojo.tools.wazuh.v4_8 import WazuhV4_8 class WazuhParser: @@ -22,74 +22,12 @@ def get_description_for_scan_types(self, scan_type): def get_findings(self, file, test): data = json.load(file) - if not data: return [] - # Detect duplications - dupes = {} - # Loop through each element in the list - vulnerabilities = data.get("data", {}).get("affected_items", []) - for item in vulnerabilities: - if ( - item["condition"] != "Package unfixed" - and item["severity"] != "Untriaged" - ): - cve = item.get("cve") - package_name = item.get("name") - package_version = item.get("version") - description = item.get("condition") - severity = item.get("severity").capitalize() - agent_ip = item.get("agent_ip") - links = item.get("external_references") - cvssv3_score = item.get("cvss3_score") - publish_date = item.get("published") - agent_name = item.get("agent_name") - agent_ip = item.get("agent_ip") - detection_time = item.get("detection_time").split("T")[0] - - references = "\n".join(links) if links else None - - title = ( - item.get("title") + " (version: " + package_version + ")" - ) - - if agent_name: - dupe_key = title + cve + agent_name + package_name + package_version - else: - dupe_key = title + cve + package_name + package_version - 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, - description=description, - severity=severity, - references=references, - static_finding=True, - component_name=package_name, - component_version=package_version, - cvssv3_score=cvssv3_score, - publish_date=publish_date, - unique_id_from_tool=dupe_key, - date=detection_time, - ) - - # in some cases the agent_ip is not the perfect way on how to identify a host. Thus prefer the agent_name, if existant. - if agent_name: - find.unsaved_endpoints = [Endpoint(host=agent_name)] - elif agent_ip: - find.unsaved_endpoints = [Endpoint(host=agent_ip)] - - if id: - find.unsaved_vulnerability_ids = cve - - dupes[dupe_key] = find - - return list(dupes.values()) + if data.get("data"): + return WazuhV4_7().parse_findings(test, data) + if data.get("hits"): + return WazuhV4_8().parse_findings(test, data) + return [] diff --git a/dojo/tools/wazuh/v4_7.py b/dojo/tools/wazuh/v4_7.py new file mode 100644 index 00000000000..1357571d0d5 --- /dev/null +++ b/dojo/tools/wazuh/v4_7.py @@ -0,0 +1,70 @@ +import hashlib + +from dojo.models import Endpoint, Finding + + +class WazuhV4_7: + def parse_findings(self, test, data): + dupes = {} + vulnerabilities = data.get("data", {}).get("affected_items", []) + for item in vulnerabilities: + if ( + item["condition"] != "Package unfixed" + and item["severity"] != "Untriaged" + ): + cve = item.get("cve") + package_name = item.get("name") + package_version = item.get("version") + description = item.get("condition") + severity = item.get("severity").capitalize() + agent_ip = item.get("agent_ip") + links = item.get("external_references") + cvssv3_score = item.get("cvss3_score") + publish_date = item.get("published") + agent_name = item.get("agent_name") + agent_ip = item.get("agent_ip") + detection_time = item.get("detection_time").split("T")[0] + + references = "\n".join(links) if links else None + + title = ( + item.get("title") + " (version: " + package_version + ")" + ) + + if agent_name: + dupe_key = title + cve + agent_name + package_name + package_version + else: + dupe_key = title + cve + package_name + package_version + 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, + description=description, + severity=severity, + references=references, + static_finding=True, + component_name=package_name, + component_version=package_version, + cvssv3_score=cvssv3_score, + publish_date=publish_date, + unique_id_from_tool=dupe_key, + date=detection_time, + ) + + # in some cases the agent_ip is not the perfect way on how to identify a host. Thus prefer the agent_name, if existant. + if agent_name: + find.unsaved_endpoints = [Endpoint(host=agent_name)] + elif agent_ip: + find.unsaved_endpoints = [Endpoint(host=agent_ip)] + + if id: + find.unsaved_vulnerability_ids = cve + + dupes[dupe_key] = find + return list(dupes.values()) diff --git a/dojo/tools/wazuh/v4_8.py b/dojo/tools/wazuh/v4_8.py new file mode 100644 index 00000000000..3192e38d231 --- /dev/null +++ b/dojo/tools/wazuh/v4_8.py @@ -0,0 +1,53 @@ +import hashlib + +from dojo.models import Finding + + +class WazuhV4_8: + def parse_findings(self, test, data): + dupes = {} + vulnerabilities = data.get("hits", {}).get("hits", []) + for item_source in vulnerabilities: + item = item_source.get("_source") + vuln = item.get("vulnerability") + cve = vuln.get("id") + 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, + description=description, + severity=severity, + references=references, + static_finding=True, + component_name=item.get("package").get("name"), + component_version=item.get("package").get("version"), + cvssv3_score=cvssv3_score, + publish_date=publish_date, + unique_id_from_tool=dupe_key, + date=detection_time, + ) + find.unsaved_vulnerability_ids = cve + dupes[dupe_key] = find + return list(dupes.values()) diff --git a/unittests/scans/wazuh/many_findings.json b/unittests/scans/wazuh/v4-7_many_findings.json similarity index 100% rename from unittests/scans/wazuh/many_findings.json rename to unittests/scans/wazuh/v4-7_many_findings.json diff --git a/unittests/scans/wazuh/no_findings.json b/unittests/scans/wazuh/v4-7_no_findings.json similarity index 100% rename from unittests/scans/wazuh/no_findings.json rename to unittests/scans/wazuh/v4-7_no_findings.json diff --git a/unittests/scans/wazuh/one_finding.json b/unittests/scans/wazuh/v4-7_one_finding.json similarity index 100% rename from unittests/scans/wazuh/one_finding.json rename to unittests/scans/wazuh/v4-7_one_finding.json diff --git a/unittests/scans/wazuh/one_finding_with_endpoint.json b/unittests/scans/wazuh/v4-7_one_finding_with_endpoint.json similarity index 100% rename from unittests/scans/wazuh/one_finding_with_endpoint.json rename to unittests/scans/wazuh/v4-7_one_finding_with_endpoint.json diff --git a/unittests/scans/wazuh/v4-8_many_findings.json b/unittests/scans/wazuh/v4-8_many_findings.json new file mode 100644 index 00000000000..4b0e0f8b25d --- /dev/null +++ b/unittests/scans/wazuh/v4-8_many_findings.json @@ -0,0 +1,629 @@ +{ + "took": 8, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 125, + "relation": "eq" + }, + "max_score": 5.596354, + "hits": [ + { + "_index": "wazuh-states-vulnerabilities-wazuh-server", + "_id": "001_b5f8c1a3d7e902b4c6d8e0f2a4b6c8d0e2f4a6b8_CVE-2025-27558", + "_score": 5.596354, + "_source": { + "agent": { + "id": "001", + "name": "myhost0", + "type": "Wazuh", + "version": "v4.11.1" + }, + "host": { + "os": { + "full": "Ubuntu 24.04.2 LTS (Noble Numbat)", + "kernel": "6.8.0-62-generic", + "name": "Ubuntu", + "platform": "ubuntu", + "type": "ubuntu", + "version": "24.04.2" + } + }, + "package": { + "architecture": "amd64", + "description": "Signed kernel image generic", + "name": "linux-image-6.8.0-60-generic", + "size": 15025152, + "type": "deb", + "version": "6.8.0-60.63" + }, + "vulnerability": { + "category": "Packages", + "classification": "-", + "description": "IEEE P802.11-REVme D1.1 through D7.0 allows FragAttacks against meshnetworks. In mesh networks using Wi-Fi Protected Access (WPA, WPA2, orWPA3) or Wired Equivalent Privacy (WEP), an adversary can exploit thisvulnerability to inject arbitrary frames towards devices that supportreceiving non-SSP A-MSDU frames. NOTE: this issue exists because of anincorrect fix for CVE-2020-24588. P802.11-REVme, as of early 2025, is aplanned release of the 802.11 standard.", + "detected_at": "2025-06-30T17:07:15.204Z", + "enumeration": "CVE", + "id": "CVE-2025-27558", + "published_at": "2025-05-21T19:16:08Z", + "reference": "https://ubuntu.com/security/CVE-2025-27558, https://www.cve.org/CVERecord?id=CVE-2025-27558", + "scanner": { + "condition": "Package default status", + "reference": "https://cti.wazuh.com/vulnerabilities/cves/CVE-2025-27558", + "source": "Canonical Security Tracker", + "vendor": "Wazuh" + }, + "score": { + "base": 9.1, + "version": "3.1" + }, + "severity": "Critical", + "under_evaluation": false + }, + "wazuh": { + "cluster": { + "name": "wazuh-server" + }, + "schema": { + "version": "1.0.0" + } + } + } + }, + { + "_index": "wazuh-states-vulnerabilities-wazuh-server", + "_id": "001_b5f8c1a3d7e902b4c6d8e0f2a4b6c8d0e2f4a6b8_CVE-2024-56180", + "_score": 5.596354, + "_source": { + "agent": { + "id": "0201", + "name": "myhost0", + "type": "Wazuh", + "version": "v4.11.1" + }, + "host": { + "os": { + "full": "Ubuntu 24.04.2 LTS (Noble Numbat)", + "kernel": "6.8.0-62-generic", + "name": "Ubuntu", + "platform": "ubuntu", + "type": "ubuntu", + "version": "24.04.2" + } + }, + "package": { + "architecture": "amd64", + "description": "Signed kernel image generic", + "name": "linux-image-6.8.0-60-generic", + "size": 15025152, + "type": "deb", + "version": "6.8.0-60.63" + }, + "vulnerability": { + "category": "Packages", + "classification": "-", + "description": "CWE-502 Deserialization of Untrusted Data at theeventmesh-meta-raft plugin module in Apache EventMesh master branch withoutrelease version on windows\\linux\\mac os e.g. platforms allows attackers tosend controlled message and remote code execute via hessian deserializationrpc protocol. Users can use the code under the master branch in projectrepo or version 1.11.0 to fix this issue.", + "detected_at": "2025-06-30T17:07:15.217Z", + "enumeration": "CVE", + "id": "CVE-2024-56180", + "published_at": "2025-02-14T14:15:32Z", + "reference": "https://ubuntu.com/security/CVE-2024-56180, https://www.cve.org/CVERecord?id=CVE-2024-56180", + "scanner": { + "condition": "Package default status", + "reference": "https://cti.wazuh.com/vulnerabilities/cves/CVE-2024-56180", + "source": "Canonical Security Tracker", + "vendor": "Wazuh" + }, + "score": { + "base": 9.8, + "version": "3.1" + }, + "severity": "Critical", + "under_evaluation": false + }, + "wazuh": { + "cluster": { + "name": "wazuh-server" + }, + "schema": { + "version": "1.0.0" + } + } + } + }, + { + "_index": "wazuh-states-vulnerabilities-wazuh-server", + "_id": "001_b5f8c1a3d7e902b4c6d8e0f2a4b6c8d0e2f4a6b8_CVE-2021-3773", + "_score": 5.596354, + "_source": { + "agent": { + "id": "4001", + "name": "myhost0", + "type": "Wazuh", + "version": "v4.11.1" + }, + "host": { + "os": { + "full": "Ubuntu 24.04.2 LTS (Noble Numbat)", + "kernel": "6.8.0-62-generic", + "name": "Ubuntu", + "platform": "ubuntu", + "type": "ubuntu", + "version": "24.04.2" + } + }, + "package": { + "architecture": "amd64", + "description": "Signed kernel image generic", + "name": "linux-image-6.8.0-60-generic", + "size": 15025152, + "type": "deb", + "version": "6.8.0-60.63" + }, + "vulnerability": { + "category": "Packages", + "classification": "-", + "description": "A flaw in netfilter could allow a network-connected attacker to inferopenvpn connection endpoint information for further use in traditionalnetwork attacks.", + "detected_at": "2025-06-30T17:07:15.218Z", + "enumeration": "CVE", + "id": "CVE-2021-3773", + "published_at": "2022-02-16T19:15:08Z", + "reference": "https://ubuntu.com/security/CVE-2021-3773, https://www.cve.org/CVERecord?id=CVE-2021-3773", + "scanner": { + "condition": "Package default status", + "reference": "https://cti.wazuh.com/vulnerabilities/cves/CVE-2021-3773", + "source": "Canonical Security Tracker", + "vendor": "Wazuh" + }, + "score": { + "base": 9.8, + "version": "3.1" + }, + "severity": "Critical", + "under_evaluation": false + }, + "wazuh": { + "cluster": { + "name": "wazuh-server" + }, + "schema": { + "version": "1.0.0" + } + } + } + }, + { + "_index": "wazuh-states-vulnerabilities-wazuh-server", + "_id": "001_b5f8c1a3d7e902b4c6d8e0f2a4b6c8d0e2f4a6b8_CVE-2025-27558", + "_score": 5.596354, + "_source": { + "agent": { + "id": "0016", + "name": "myhost0", + "type": "Wazuh", + "version": "v4.11.1" + }, + "host": { + "os": { + "full": "Ubuntu 24.04.2 LTS (Noble Numbat)", + "kernel": "6.8.0-62-generic", + "name": "Ubuntu", + "platform": "ubuntu", + "type": "ubuntu", + "version": "24.04.2" + } + }, + "package": { + "architecture": "amd64", + "description": "Signed kernel image generic", + "name": "linux-image-6.8.0-62-generic", + "size": 15033344, + "type": "deb", + "version": "6.8.0-62.65" + }, + "vulnerability": { + "category": "Packages", + "classification": "-", + "description": "IEEE P802.11-REVme D1.1 through D7.0 allows FragAttacks against meshnetworks. In mesh networks using Wi-Fi Protected Access (WPA, WPA2, orWPA3) or Wired Equivalent Privacy (WEP), an adversary can exploit thisvulnerability to inject arbitrary frames towards devices that supportreceiving non-SSP A-MSDU frames. NOTE: this issue exists because of anincorrect fix for CVE-2020-24588. P802.11-REVme, as of early 2025, is aplanned release of the 802.11 standard.", + "detected_at": "2025-06-30T17:07:15.926Z", + "enumeration": "CVE", + "id": "CVE-2025-27558", + "published_at": "2025-05-21T19:16:08Z", + "reference": "https://ubuntu.com/security/CVE-2025-27558, https://www.cve.org/CVERecord?id=CVE-2025-27558", + "scanner": { + "condition": "Package default status", + "reference": "https://cti.wazuh.com/vulnerabilities/cves/CVE-2025-27558", + "source": "Canonical Security Tracker", + "vendor": "Wazuh" + }, + "score": { + "base": 9.1, + "version": "3.1" + }, + "severity": "Critical", + "under_evaluation": false + }, + "wazuh": { + "cluster": { + "name": "wazuh-server" + }, + "schema": { + "version": "1.0.0" + } + } + } + }, + { + "_index": "wazuh-states-vulnerabilities-wazuh-server", + "_id": "001_b5f8c1a3d7e902b4c6d8e0f2a4b6c8d0e2f4a6b8_CVE-2024-56180", + "_score": 5.596354, + "_source": { + "agent": { + "id": "0701", + "name": "myhost0", + "type": "Wazuh", + "version": "v4.11.1" + }, + "host": { + "os": { + "full": "Ubuntu 24.04.2 LTS (Noble Numbat)", + "kernel": "6.8.0-62-generic", + "name": "Ubuntu", + "platform": "ubuntu", + "type": "ubuntu", + "version": "24.04.2" + } + }, + "package": { + "architecture": "amd64", + "description": "Signed kernel image generic", + "name": "linux-image-6.8.0-62-generic", + "size": 15033344, + "type": "deb", + "version": "6.8.0-62.65" + }, + "vulnerability": { + "category": "Packages", + "classification": "-", + "description": "CWE-502 Deserialization of Untrusted Data at theeventmesh-meta-raft plugin module in Apache EventMesh master branch withoutrelease version on windows\\linux\\mac os e.g. platforms allows attackers tosend controlled message and remote code execute via hessian deserializationrpc protocol. Users can use the code under the master branch in projectrepo or version 1.11.0 to fix this issue.", + "detected_at": "2025-06-30T17:07:15.934Z", + "enumeration": "CVE", + "id": "CVE-2024-56180", + "published_at": "2025-02-14T14:15:32Z", + "reference": "https://ubuntu.com/security/CVE-2024-56180, https://www.cve.org/CVERecord?id=CVE-2024-56180", + "scanner": { + "condition": "Package default status", + "reference": "https://cti.wazuh.com/vulnerabilities/cves/CVE-2024-56180", + "source": "Canonical Security Tracker", + "vendor": "Wazuh" + }, + "score": { + "base": 9.8, + "version": "3.1" + }, + "severity": "Critical", + "under_evaluation": false + }, + "wazuh": { + "cluster": { + "name": "wazuh-server" + }, + "schema": { + "version": "1.0.0" + } + } + } + }, + { + "_index": "wazuh-states-vulnerabilities-wazuh-server", + "_id": "001_b5f8c1a3d7e902b4c6d8e0f2a4b6c8d0e2f4a6b8_CVE-2021-3773", + "_score": 5.596354, + "_source": { + "agent": { + "id": "0037", + "name": "myhost0", + "type": "Wazuh", + "version": "v4.11.1" + }, + "host": { + "os": { + "full": "Ubuntu 24.04.2 LTS (Noble Numbat)", + "kernel": "6.8.0-62-generic", + "name": "Ubuntu", + "platform": "ubuntu", + "type": "ubuntu", + "version": "24.04.2" + } + }, + "package": { + "architecture": "amd64", + "description": "Signed kernel image generic", + "name": "linux-image-6.8.0-62-generic", + "size": 15033344, + "type": "deb", + "version": "6.8.0-62.65" + }, + "vulnerability": { + "category": "Packages", + "classification": "-", + "description": "A flaw in netfilter could allow a network-connected attacker to inferopenvpn connection endpoint information for further use in traditionalnetwork attacks.", + "detected_at": "2025-06-30T17:07:15.935Z", + "enumeration": "CVE", + "id": "CVE-2021-3773", + "published_at": "2022-02-16T19:15:08Z", + "reference": "https://ubuntu.com/security/CVE-2021-3773, https://www.cve.org/CVERecord?id=CVE-2021-3773", + "scanner": { + "condition": "Package default status", + "reference": "https://cti.wazuh.com/vulnerabilities/cves/CVE-2021-3773", + "source": "Canonical Security Tracker", + "vendor": "Wazuh" + }, + "score": { + "base": 9.8, + "version": "3.1" + }, + "severity": "Critical", + "under_evaluation": false + }, + "wazuh": { + "cluster": { + "name": "wazuh-server" + }, + "schema": { + "version": "1.0.0" + } + } + } + }, + { + "_index": "wazuh-states-vulnerabilities-wazuh-server", + "_id": "002_b5f8c1a3d7e902b4c6d8e0f2a4b6c8d0e2f4a6b8_CVE-2022-44640", + "_score": 5.596354, + "_source": { + "agent": { + "id": "0012", + "name": "myhost1", + "type": "Wazuh", + "version": "v4.11.1" + }, + "host": { + "os": { + "full": "Ubuntu 24.04.1 LTS (Noble Numbat)", + "kernel": "6.8.0-62-generic", + "name": "Ubuntu", + "platform": "ubuntu", + "type": "ubuntu", + "version": "24.04.1" + } + }, + "package": { + "architecture": "amd64", + "description": "Heimdal Kerberos - Base library", + "name": "libheimbase1t64-heimdal", + "size": 95232, + "type": "deb", + "version": "7.8.git20221117.28daf24+dfsg-5ubuntu3" + }, + "vulnerability": { + "category": "Packages", + "classification": "-", + "description": "Heimdal before 7.7.1 allows remote attackers to execute arbitrary codebecause of an invalid free in the ASN.1 codec used by the Key DistributionCenter (KDC).", + "detected_at": "2025-06-30T17:07:16.011Z", + "enumeration": "CVE", + "id": "CVE-2022-44640", + "published_at": "2022-12-25T05:15:11Z", + "reference": "https://ubuntu.com/security/CVE-2022-44640, https://ubuntu.com/security/notices/USN-5800-1, https://www.cve.org/CVERecord?id=CVE-2022-44640", + "scanner": { + "condition": "Package default status", + "reference": "https://cti.wazuh.com/vulnerabilities/cves/CVE-2022-44640", + "source": "Canonical Security Tracker", + "vendor": "Wazuh" + }, + "score": { + "base": 9.8, + "version": "3.1" + }, + "severity": "Critical", + "under_evaluation": false + }, + "wazuh": { + "cluster": { + "name": "wazuh-server" + }, + "schema": { + "version": "1.0.0" + } + } + } + }, + { + "_index": "wazuh-states-vulnerabilities-wazuh-server", + "_id": "002_b5f8c1a3d7e902b4c6d8e0f2a4b6c8d0e2f4a6b8_CVE-2025-27558", + "_score": 5.596354, + "_source": { + "agent": { + "id": "002", + "name": "myhost1", + "type": "Wazuh", + "version": "v4.11.1" + }, + "host": { + "os": { + "full": "Ubuntu 24.04.1 LTS (Noble Numbat)", + "kernel": "6.8.0-62-generic", + "name": "Ubuntu", + "platform": "ubuntu", + "type": "ubuntu", + "version": "24.04.1" + } + }, + "package": { + "architecture": "amd64", + "description": "Signed kernel image generic", + "name": "linux-image-6.8.0-60-generic", + "size": 15025152, + "type": "deb", + "version": "6.8.0-60.63" + }, + "vulnerability": { + "category": "Packages", + "classification": "-", + "description": "IEEE P802.11-REVme D1.1 through D7.0 allows FragAttacks against meshnetworks. In mesh networks using Wi-Fi Protected Access (WPA, WPA2, orWPA3) or Wired Equivalent Privacy (WEP), an adversary can exploit thisvulnerability to inject arbitrary frames towards devices that supportreceiving non-SSP A-MSDU frames. NOTE: this issue exists because of anincorrect fix for CVE-2020-24588. P802.11-REVme, as of early 2025, is aplanned release of the 802.11 standard.", + "detected_at": "2025-06-30T17:07:16.927Z", + "enumeration": "CVE", + "id": "CVE-2025-27558", + "published_at": "2025-05-21T19:16:08Z", + "reference": "https://ubuntu.com/security/CVE-2025-27558, https://www.cve.org/CVERecord?id=CVE-2025-27558", + "scanner": { + "condition": "Package default status", + "reference": "https://cti.wazuh.com/vulnerabilities/cves/CVE-2025-27558", + "source": "Canonical Security Tracker", + "vendor": "Wazuh" + }, + "score": { + "base": 9.1, + "version": "3.1" + }, + "severity": "Critical", + "under_evaluation": false + }, + "wazuh": { + "cluster": { + "name": "wazuh-server" + }, + "schema": { + "version": "1.0.0" + } + } + } + }, + { + "_index": "wazuh-states-vulnerabilities-wazuh-server", + "_id": "002_b5f8c1a3d7e902b4c6d8e0f2a4b6c8d0e2f4a6b8_CVE-2024-56180", + "_score": 5.596354, + "_source": { + "agent": { + "id": "002", + "name": "myhost1", + "type": "Wazuh", + "version": "v4.11.1" + }, + "host": { + "os": { + "full": "Ubuntu 24.04.1 LTS (Noble Numbat)", + "kernel": "6.8.0-62-generic", + "name": "Ubuntu", + "platform": "ubuntu", + "type": "ubuntu", + "version": "24.04.1" + } + }, + "package": { + "architecture": "amd64", + "description": "Signed kernel image generic", + "name": "linux-image-6.8.0-60-generic", + "size": 15025152, + "type": "deb", + "version": "6.8.0-60.63" + }, + "vulnerability": { + "category": "Packages", + "classification": "-", + "description": "CWE-502 Deserialization of Untrusted Data at theeventmesh-meta-raft plugin module in Apache EventMesh master branch withoutrelease version on windows\\linux\\mac os e.g. platforms allows attackers tosend controlled message and remote code execute via hessian deserializationrpc protocol. Users can use the code under the master branch in projectrepo or version 1.11.0 to fix this issue.", + "detected_at": "2025-06-30T17:07:16.937Z", + "enumeration": "CVE", + "id": "CVE-2024-56180", + "published_at": "2025-02-14T14:15:32Z", + "reference": "https://ubuntu.com/security/CVE-2024-56180, https://www.cve.org/CVERecord?id=CVE-2024-56180", + "scanner": { + "condition": "Package default status", + "reference": "https://cti.wazuh.com/vulnerabilities/cves/CVE-2024-56180", + "source": "Canonical Security Tracker", + "vendor": "Wazuh" + }, + "score": { + "base": 9.8, + "version": "3.1" + }, + "severity": "Critical", + "under_evaluation": false + }, + "wazuh": { + "cluster": { + "name": "wazuh-server" + }, + "schema": { + "version": "1.0.0" + } + } + } + }, + { + "_index": "wazuh-states-vulnerabilities-wazuh-server", + "_id": "002_b5f8c1a3d7e902b4c6d8e0f2a4b6c8d0e2f4a6b8_CVE-2021-3773", + "_score": 5.596354, + "_source": { + "agent": { + "id": "002", + "name": "myhost1", + "type": "Wazuh", + "version": "v4.11.1" + }, + "host": { + "os": { + "full": "Ubuntu 24.04.1 LTS (Noble Numbat)", + "kernel": "6.8.0-62-generic", + "name": "Ubuntu", + "platform": "ubuntu", + "type": "ubuntu", + "version": "24.04.1" + } + }, + "package": { + "architecture": "amd64", + "description": "Signed kernel image generic", + "name": "linux-image-6.8.0-60-generic", + "size": 15025152, + "type": "deb", + "version": "6.8.0-60.63" + }, + "vulnerability": { + "category": "Packages", + "classification": "-", + "description": "A flaw in netfilter could allow a network-connected attacker to inferopenvpn connection endpoint information for further use in traditionalnetwork attacks.", + "detected_at": "2025-06-30T17:07:16.938Z", + "enumeration": "CVE", + "id": "CVE-2021-3773", + "published_at": "2022-02-16T19:15:08Z", + "reference": "https://ubuntu.com/security/CVE-2021-3773, https://www.cve.org/CVERecord?id=CVE-2021-3773", + "scanner": { + "condition": "Package default status", + "reference": "https://cti.wazuh.com/vulnerabilities/cves/CVE-2021-3773", + "source": "Canonical Security Tracker", + "vendor": "Wazuh" + }, + "score": { + "base": 9.8, + "version": "3.1" + }, + "severity": "Critical", + "under_evaluation": false + }, + "wazuh": { + "cluster": { + "name": "wazuh-server" + }, + "schema": { + "version": "1.0.0" + } + } + } + } + ] + } + } \ No newline at end of file diff --git a/unittests/tools/test_wazuh_parser.py b/unittests/tools/test_wazuh_parser.py index 3c5a520a798..5f73fef4f47 100644 --- a/unittests/tools/test_wazuh_parser.py +++ b/unittests/tools/test_wazuh_parser.py @@ -5,14 +5,14 @@ class TestWazuhParser(DojoTestCase): - def test_parse_no_findings(self): - with (get_unit_tests_scans_path("wazuh") / "no_findings.json").open(encoding="utf-8") as testfile: + def test_parse_v4_7_no_findings(self): + with (get_unit_tests_scans_path("wazuh") / "v4-7_no_findings.json").open(encoding="utf-8") as testfile: parser = WazuhParser() findings = parser.get_findings(testfile, Test()) self.assertEqual(0, len(findings)) - def test_parse_one_finding(self): - with (get_unit_tests_scans_path("wazuh") / "one_finding.json").open(encoding="utf-8") as testfile: + def test_parse_v4_7_one_finding(self): + with (get_unit_tests_scans_path("wazuh") / "v4-7_one_finding.json").open(encoding="utf-8") as testfile: parser = WazuhParser() findings = parser.get_findings(testfile, Test()) for finding in findings: @@ -25,8 +25,8 @@ def test_parse_one_finding(self): self.assertEqual("4.3.1", finding.component_version) self.assertEqual(5.5, finding.cvssv3_score) - def test_parse_many_finding(self): - with (get_unit_tests_scans_path("wazuh") / "many_findings.json").open(encoding="utf-8") as testfile: + def test_parse_v4_7_many_finding(self): + with (get_unit_tests_scans_path("wazuh") / "v4-7_many_findings.json").open(encoding="utf-8") as testfile: parser = WazuhParser() findings = parser.get_findings(testfile, Test()) for finding in findings: @@ -35,8 +35,8 @@ def test_parse_many_finding(self): self.assertEqual(6, len(findings)) self.assertEqual("2023-02-08", finding.date) - def test_parse_one_finding_with_endpoint(self): - with (get_unit_tests_scans_path("wazuh") / "one_finding_with_endpoint.json").open(encoding="utf-8") as testfile: + def test_parse_v4_7_one_finding_with_endpoint(self): + with (get_unit_tests_scans_path("wazuh") / "v4-7_one_finding_with_endpoint.json").open(encoding="utf-8") as testfile: parser = WazuhParser() findings = parser.get_findings(testfile, Test()) for finding in findings: @@ -51,3 +51,12 @@ def test_parse_one_finding_with_endpoint(self): self.assertEqual("asdf", finding.component_name) self.assertEqual("1", finding.component_version) self.assertEqual("2023-12-13", finding.date) + + def test_parse_v4_8_many_findings(self): + with (get_unit_tests_scans_path("wazuh") / "v4-8_many_findings.json").open(encoding="utf-8") as testfile: + parser = WazuhParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(10, len(findings)) + self.assertEqual("CVE-2025-27558 affects (version: 6.8.0-60.63)", findings[0].title) + self.assertEqual("Critical", findings[0].severity) + self.assertEqual(9.1, findings[0].cvssv3_score) From 7d3b99993d75c5a037709eadab6fa006ee95066f Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Fri, 5 Sep 2025 21:30:16 +0200 Subject: [PATCH 13/18] :tada: Add fix_available information to jfrog (#13115) --- dojo/tools/jfrogxray/parser.py | 3 +++ unittests/tools/test_jfrogxray_parser.py | 1 + 2 files changed, 4 insertions(+) diff --git a/dojo/tools/jfrogxray/parser.py b/dojo/tools/jfrogxray/parser.py index 5a4a6fa5ad4..4dc321d036f 100644 --- a/dojo/tools/jfrogxray/parser.py +++ b/dojo/tools/jfrogxray/parser.py @@ -76,6 +76,7 @@ def get_item(vulnerability, test): cvssv3 = None cvss_v3 = "No CVSS v3 score." mitigation = None + fix_available = False extra_desc = "" # Some entries have no CVE entries, despite they exist. Example # CVE-2017-1000502. @@ -95,6 +96,7 @@ def get_item(vulnerability, test): mitigation += "\n".join( vulnerability["component_versions"]["fixed_versions"], ) + fix_available = True if "vulnerable_versions" in vulnerability["component_versions"]: extra_desc = "\n**Versions that are vulnerable:**\n" @@ -160,6 +162,7 @@ def get_item(vulnerability, test): static_finding=True, dynamic_finding=False, cvssv3=cvssv3, + fix_available=fix_available, ) if vulnerability_ids: finding.unsaved_vulnerability_ids = vulnerability_ids diff --git a/unittests/tools/test_jfrogxray_parser.py b/unittests/tools/test_jfrogxray_parser.py index b67c4a546fa..4d3cf7cf41c 100644 --- a/unittests/tools/test_jfrogxray_parser.py +++ b/unittests/tools/test_jfrogxray_parser.py @@ -17,6 +17,7 @@ def test_parse_file_with_one_vuln(self): self.assertEqual(1, len(item.unsaved_vulnerability_ids)) self.assertEqual("CVE-2018-14600", item.unsaved_vulnerability_ids[0]) self.assertEqual(787, item.cwe) + self.assertEqual(True, item.fix_available) def test_parse_file_with_many_vulns(self): testfile = (get_unit_tests_scans_path("jfrogxray") / "many_vulns.json").open(encoding="utf-8") From 25e532e93de661bda3901daf1e18d880d49bf063 Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Fri, 5 Sep 2025 21:30:36 +0200 Subject: [PATCH 14/18] :tada: Add fix_available information to jfrog xray unified parser #12633 (#13105) --- dojo/tools/jfrog_xray_unified/parser.py | 3 +++ unittests/tools/test_jfrog_xray_unified_parser.py | 1 + 2 files changed, 4 insertions(+) diff --git a/dojo/tools/jfrog_xray_unified/parser.py b/dojo/tools/jfrog_xray_unified/parser.py index 9d2f0421ba0..83e0222ade0 100644 --- a/dojo/tools/jfrog_xray_unified/parser.py +++ b/dojo/tools/jfrog_xray_unified/parser.py @@ -80,12 +80,14 @@ def get_item(vulnerability, test): if "cvss_v2_vector" in worstCve: cvss_v2 = worstCve["cvss_v2_vector"] + fix_available = False if ( "fixed_versions" in vulnerability and len(vulnerability["fixed_versions"]) > 0 ): mitigation = "Versions containing a fix:\n" mitigation += "\n".join(vulnerability["fixed_versions"]) + fix_available = True if ( "external_advisory_source" in vulnerability @@ -138,6 +140,7 @@ def get_item(vulnerability, test): date=scan_time, unique_id_from_tool=vulnerability["issue_id"], tags=tags, + fix_available=fix_available, ) cvss_data = parse_cvss_data(cvssv3) diff --git a/unittests/tools/test_jfrog_xray_unified_parser.py b/unittests/tools/test_jfrog_xray_unified_parser.py index ca02a12162a..52b673308c4 100644 --- a/unittests/tools/test_jfrog_xray_unified_parser.py +++ b/unittests/tools/test_jfrog_xray_unified_parser.py @@ -27,6 +27,7 @@ def test_parse_file_with_one_vuln(self): self.assertEqual(1, len(item.unsaved_vulnerability_ids)) self.assertEqual("CVE-2020-28493", item.unsaved_vulnerability_ids[0]) self.assertEqual("Medium", item.severity) + self.assertEqual(True, item.fix_available) self.assertEqual("This affects the package", item.description[:24]) self.assertEqual(" memory.", item.description[-8:]) self.assertIsNotNone(item.mitigation) From 18330a94ebc4f6af931f3cc5335a993671e4ecde Mon Sep 17 00:00:00 2001 From: valentijnscholten Date: Fri, 5 Sep 2025 21:47:39 +0200 Subject: [PATCH 15/18] ruff (#13122) --- dojo/tools/semgrep_pro/parser.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dojo/tools/semgrep_pro/parser.py b/dojo/tools/semgrep_pro/parser.py index d4371073f5b..c8da673d967 100644 --- a/dojo/tools/semgrep_pro/parser.py +++ b/dojo/tools/semgrep_pro/parser.py @@ -139,11 +139,11 @@ def get_findings(self, filename, test): def convert_severity(self, val): val = val.upper() - if val == "ERROR" or val == "HIGH": + if val in {"ERROR", "HIGH"}: return "High" - if val == "WARNING" or val == "MEDIUM": + if val in {"WARNING", "MEDIUM"}: return "Medium" - if val == "INFO" or val == "LOW": + if val in {"INFO", "LOW"}: return "Low" if val == "CRITICAL": return "Critical" From 5ada3e3392d4b485c5c96caf9478b7560e2bf541 Mon Sep 17 00:00:00 2001 From: manuelsommer <47991713+manuel-sommer@users.noreply.github.com> Date: Fri, 5 Sep 2025 22:56:11 +0200 Subject: [PATCH 16/18] :bug: Fix finding_group view (#13119) * :bug: Fix finding_group view * ruff * finding group view: add basic UI tests --------- Co-authored-by: Valentijn Scholten --- dojo/finding_group/views.py | 1 + tests/check_various_pages.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/dojo/finding_group/views.py b/dojo/finding_group/views.py index 14b82bbb6c8..9ba6b546bf4 100644 --- a/dojo/finding_group/views.py +++ b/dojo/finding_group/views.py @@ -285,6 +285,7 @@ def get_finding_groups(self, request: HttpRequest, products: QuerySet[Product] | def paginate_queryset(self, queryset: QuerySet[Finding_Group], request: HttpRequest) -> Page: page_size = int(request.GET.get("page_size", 25)) + queryset = queryset.order_by("id") paginator = Paginator(queryset, page_size) page_number = request.GET.get("page") return paginator.get_page(page_number) diff --git a/tests/check_various_pages.py b/tests/check_various_pages.py index 68a5f22066d..c78c7c596d0 100644 --- a/tests/check_various_pages.py +++ b/tests/check_various_pages.py @@ -16,12 +16,32 @@ def test_calendar_status(self): # click apply to see if this helps webdriver to catch the javascript errors we're seeing driver.find_element(By.CSS_SELECTOR, "input.btn.btn-primary").click() + def test_finding_group_open_status(self): + driver = self.driver + driver.get(self.base_url + "finding_group/open") + + def test_finding_group_all_status(self): + driver = self.driver + driver.get(self.base_url + "finding_group/all") + + def test_finding_group_closed_status(self): + driver = self.driver + driver.get(self.base_url + "finding_group/closed") + + 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 suite(): suite = unittest.TestSuite() suite.addTest(BaseTestCase("test_login")) suite.addTest(VariousPagesTest("test_user_status")) suite.addTest(VariousPagesTest("test_calendar_status")) + suite.addTest(VariousPagesTest("test_finding_group_open_status")) + 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")) return suite From aeb7c4ab455572047e13487331240e566a1c5ea0 Mon Sep 17 00:00:00 2001 From: Leonardo de Oliveira Maia <101437950+LeoOMaia@users.noreply.github.com> Date: Sat, 6 Sep 2025 16:44:55 -0300 Subject: [PATCH 17/18] fix default order to finding_groups --- dojo/finding_group/views.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dojo/finding_group/views.py b/dojo/finding_group/views.py index 9ba6b546bf4..7f932cc0f3d 100644 --- a/dojo/finding_group/views.py +++ b/dojo/finding_group/views.py @@ -236,8 +236,8 @@ def order_field(self, request: HttpRequest, group_findings_queryset: QuerySet[Fi order_field_param = order_field_param[1:] if reverse_order else order_field_param if order_field_param in {"name", "creator", "findings_count", "sla_deadline"}: prefix = "-" if reverse_order else "" - group_findings_queryset = group_findings_queryset.order_by(f"{prefix}{order_field_param}") - return group_findings_queryset + return group_findings_queryset.order_by(f"{prefix}{order_field_param}") + return group_findings_queryset.order_by("id") def filters(self, request: HttpRequest) -> tuple[str, str | None, list[str], list[str]]: name_filter: str = request.GET.get("name", "").lower() @@ -285,7 +285,6 @@ def get_finding_groups(self, request: HttpRequest, products: QuerySet[Product] | def paginate_queryset(self, queryset: QuerySet[Finding_Group], request: HttpRequest) -> Page: page_size = int(request.GET.get("page_size", 25)) - queryset = queryset.order_by("id") paginator = Paginator(queryset, page_size) page_number = request.GET.get("page") return paginator.get_page(page_number) From f72cb99e42b4548cb8c95555ba04a463544f2bae Mon Sep 17 00:00:00 2001 From: DefectDojo release bot Date: Mon, 8 Sep 2025 13:37:20 +0000 Subject: [PATCH 18/18] 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..bf7bc889846 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "defectdojo", - "version": "2.51.0-dev", + "version": "2.50.1", "license" : "BSD-3-Clause", "private": true, "dependencies": { diff --git a/dojo/__init__.py b/dojo/__init__.py index d5f0f80913c..82fe1512626 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.0" +__version__ = "2.50.1" __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 f2617ac4715..eaba06160a6 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.1" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.6.206-dev +version: 1.6.206 icon: https://www.defectdojo.org/img/favicon.ico maintainers: - name: madchap