Skip to content

Commit 6d3207f

Browse files
Merge remote-tracking branch 'upstream/dev' into pghistory
2 parents b0277e4 + 57690b9 commit 6d3207f

22 files changed

Lines changed: 777 additions & 277 deletions

.github/workflows/k8s-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
# are tested (https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html#available-versions)
2828
- databases: pgsql
2929
brokers: redis
30-
k8s: 'v1.34.1'
30+
k8s: 'v1.34.0'
3131
os: debian
3232
steps:
3333
- name: Checkout

docker/entrypoint-unit-tests.sh

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,16 @@ echo "Unit Tests"
8080
echo "------------------------------------------------------------"
8181

8282
# Removing parallel and shuffle for now to maintain stability
83-
python3 manage.py test unittests -v 3 --keepdb --no-input --exclude-tag="non-parallel" || {
83+
python3 manage.py test unittests -v 3 --keepdb --no-input --exclude-tag="non-parallel" --exclude-tag="transactional" || {
8484
exit 1;
8585
}
8686
python3 manage.py test unittests -v 3 --keepdb --no-input --tag="non-parallel" || {
8787
exit 1;
88-
}
88+
}
89+
# Running one unit tests that inherits from TransactionTestCase somehow changes the behaviour of how Django loads fixtures into the database.
90+
# Meaning any test after this one would fail to load our dojo_testdata.json fixture. In a way this makes sense as it contains some data integrity problems.
91+
# I tried to fix these in https://github.com/DefectDojo/django-DefectDojo/pull/13217.
92+
# For now here we run the only TranscationTestCase at the end to avoid the problem.
93+
python3 manage.py test unittests -v 3 --keepdb --no-input --tag="transactional" || {
94+
exit 1;
95+
}

docs/content/en/changelog/changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ For Open Source release notes, please see the [Releases page on GitHub](https://
1010

1111
## Sept 2025: v2.50
1212

13+
### Sept 15, 2025: v2.50.3
14+
15+
* **(Pro UI)** Added support for [CVSSv4.0](https://www.first.org/cvss/v4-0/) vector strings.
16+
1317
### Sept 15, 2025: v2.50.2
1418

1519
* **(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.

dojo/api_v2/serializers.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from rest_framework.exceptions import ValidationError as RestFrameworkValidationError
2323
from rest_framework.fields import DictField, MultipleChoiceField
2424

25+
import dojo.finding.helper as finding_helper
2526
import dojo.jira_link.helper as jira_helper
2627
import dojo.risk_acceptance.helper as ra_helper
2728
from dojo.authorization.authorization import user_has_permission
@@ -122,6 +123,7 @@
122123
requires_file,
123124
requires_tool_type,
124125
)
126+
from dojo.user.queries import get_authorized_users
125127
from dojo.user.utils import get_configuration_permissions_codenames
126128
from dojo.utils import is_scan_file_too_large
127129
from dojo.validators import ImporterFileExtensionValidator, tag_validator
@@ -2697,6 +2699,9 @@ class FindingCloseSerializer(serializers.ModelSerializer):
26972699
false_p = serializers.BooleanField(required=False)
26982700
out_of_scope = serializers.BooleanField(required=False)
26992701
duplicate = serializers.BooleanField(required=False)
2702+
mitigated_by = serializers.PrimaryKeyRelatedField(required=False, allow_null=True, queryset=Dojo_User.objects.all())
2703+
note = serializers.CharField(required=False, allow_blank=True)
2704+
note_type = serializers.PrimaryKeyRelatedField(required=False, allow_null=True, queryset=Note_Type.objects.all())
27002705

27012706
class Meta:
27022707
model = Finding
@@ -2706,8 +2711,34 @@ class Meta:
27062711
"false_p",
27072712
"out_of_scope",
27082713
"duplicate",
2714+
"mitigated_by",
2715+
"note",
2716+
"note_type",
27092717
)
27102718

2719+
def validate(self, data):
2720+
request = self.context.get("request")
2721+
request_user = getattr(request, "user", None)
2722+
2723+
mitigated_by_user = data.get("mitigated_by")
2724+
if mitigated_by_user is not None:
2725+
# Require permission to edit mitigated metadata
2726+
if not (request_user and finding_helper.can_edit_mitigated_data(request_user)):
2727+
raise serializers.ValidationError({
2728+
"mitigated_by": ["Not allowed to set mitigated_by."],
2729+
})
2730+
2731+
# Ensure selected user is authorized (Finding_Edit)
2732+
authorized_users = get_authorized_users(Permissions.Finding_Edit, user=request_user)
2733+
if not authorized_users.filter(id=mitigated_by_user.id).exists():
2734+
raise serializers.ValidationError({
2735+
"mitigated_by": [
2736+
"Selected user is not authorized to be set as mitigated_by.",
2737+
],
2738+
})
2739+
2740+
return data
2741+
27112742

27122743
class ReportGenerateOptionSerializer(serializers.Serializer):
27132744
include_finding_notes = serializers.BooleanField(default=False)

dojo/api_v2/views.py

Lines changed: 15 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated
3232
from rest_framework.response import Response
3333

34+
import dojo.finding.helper as finding_helper
3435
import dojo.jira_link.helper as jira_helper
3536
from dojo.api_v2 import (
3637
mixins as dojo_mixins,
@@ -924,49 +925,27 @@ def close(self, request, pk=None):
924925
if request.method == "POST":
925926
finding_close = serializers.FindingCloseSerializer(
926927
data=request.data,
928+
context={"request": request},
927929
)
928930
if finding_close.is_valid():
929-
finding.is_mitigated = finding_close.validated_data[
930-
"is_mitigated"
931-
]
932-
if settings.EDITABLE_MITIGATED_DATA:
933-
finding.mitigated = (
934-
finding_close.validated_data["mitigated"]
935-
or timezone.now()
936-
)
937-
else:
938-
finding.mitigated = timezone.now()
939-
finding.mitigated_by = request.user
940-
finding.active = False
941-
finding.false_p = finding_close.validated_data.get(
942-
"false_p", False,
943-
)
944-
finding.duplicate = finding_close.validated_data.get(
945-
"duplicate", False,
946-
)
947-
finding.out_of_scope = finding_close.validated_data.get(
948-
"out_of_scope", False,
931+
# Use shared helper to perform close operations
932+
finding_helper.close_finding(
933+
finding=finding,
934+
user=request.user,
935+
is_mitigated=finding_close.validated_data["is_mitigated"],
936+
mitigated=(finding_close.validated_data.get("mitigated") if settings.EDITABLE_MITIGATED_DATA else timezone.now()),
937+
mitigated_by=finding_close.validated_data.get("mitigated_by") or (request.user if not finding_helper.can_edit_mitigated_data(request.user) else None),
938+
false_p=finding_close.validated_data.get("false_p", False),
939+
out_of_scope=finding_close.validated_data.get("out_of_scope", False),
940+
duplicate=finding_close.validated_data.get("duplicate", False),
941+
note_entry=finding_close.validated_data.get("note"),
942+
note_type=finding_close.validated_data.get("note_type"),
949943
)
950-
951-
endpoints_status = finding.status_finding.all()
952-
for e_status in endpoints_status:
953-
e_status.mitigated_by = request.user
954-
if settings.EDITABLE_MITIGATED_DATA:
955-
e_status.mitigated_time = (
956-
finding_close.validated_data["mitigated"]
957-
or timezone.now()
958-
)
959-
else:
960-
e_status.mitigated_time = timezone.now()
961-
e_status.mitigated = True
962-
e_status.last_modified = timezone.now()
963-
e_status.save()
964-
finding.save()
965944
else:
966945
return Response(
967946
finding_close.errors, status=status.HTTP_400_BAD_REQUEST,
968947
)
969-
serialized_finding = serializers.FindingCloseSerializer(finding)
948+
serialized_finding = serializers.FindingCloseSerializer(finding, context={"request": request})
970949
return Response(serialized_finding.data)
971950

972951
@extend_schema(

dojo/finding/helper.py

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
from django.db.models.signals import post_delete, pre_delete
88
from django.db.utils import IntegrityError
99
from django.dispatch.dispatcher import receiver
10+
from django.urls import reverse
1011
from django.utils import timezone
1112
from fieldsignals import pre_save_changed
1213

1314
import dojo.jira_link.helper as jira_helper
15+
import dojo.risk_acceptance.helper as ra_helper
1416
from dojo.celery import app
1517
from dojo.decorators import dojo_async_task, dojo_model_from_id, dojo_model_to_id
1618
from dojo.endpoint.utils import save_endpoints_to_add
@@ -21,15 +23,18 @@
2123
Engagement,
2224
Finding,
2325
Finding_Group,
26+
Notes,
2427
System_Settings,
2528
Test,
2629
Vulnerability_Id,
2730
Vulnerability_Id_Template,
2831
)
2932
from dojo.notes.helper import delete_related_notes
33+
from dojo.notifications.helper import create_notification
3034
from dojo.tools import tool_issue_updater
3135
from dojo.utils import (
3236
calculate_grade,
37+
close_external_issue,
3338
do_dedupe_finding,
3439
do_false_positive_history,
3540
get_current_user,
@@ -161,6 +166,24 @@ def update_finding_status(new_state_finding, user, changed_fields=None):
161166
new_state_finding.last_status_update = now
162167

163168

169+
def filter_findings_by_existence(findings):
170+
"""
171+
Return only findings that still exist in the database (by id).
172+
173+
Centralized helper used by importers to avoid FK violations during
174+
bulk_create.
175+
"""
176+
if not findings:
177+
return []
178+
candidate_ids = [finding.id for finding in findings if getattr(finding, "id", None)]
179+
if not candidate_ids:
180+
return []
181+
existing_ids = set(
182+
Finding.objects.filter(id__in=candidate_ids).values_list("id", flat=True),
183+
)
184+
return [finding for finding in findings if finding.id in existing_ids]
185+
186+
164187
def can_edit_mitigated_data(user):
165188
return settings.EDITABLE_MITIGATED_DATA and user.is_superuser
166189

@@ -271,7 +294,6 @@ def get_group_by_group_name(finding, finding_group_by_option):
271294
else:
272295
msg = f"Invalid group_by option {finding_group_by_option}"
273296
raise ValueError(msg)
274-
275297
if group_name:
276298
return f"Findings in: {group_name}"
277299

@@ -689,3 +711,91 @@ def save_vulnerability_ids_template(finding_template, vulnerability_ids):
689711
finding_template.cve = vulnerability_ids[0]
690712
else:
691713
finding_template.cve = None
714+
715+
716+
def close_finding(
717+
*,
718+
finding,
719+
user,
720+
is_mitigated,
721+
mitigated,
722+
mitigated_by,
723+
false_p,
724+
out_of_scope,
725+
duplicate,
726+
note_entry=None,
727+
note_type=None,
728+
) -> None:
729+
"""
730+
Shared close logic used by UI and API.
731+
732+
Handles status updates, endpoint statuses, risk acceptance, external issues,
733+
JIRA sync, and notification.
734+
"""
735+
# Core status updates
736+
finding.is_mitigated = is_mitigated
737+
now = timezone.now()
738+
finding.mitigated = mitigated or now
739+
finding.mitigated_by = mitigated_by or user
740+
finding.active = False
741+
finding.false_p = bool(false_p)
742+
finding.out_of_scope = bool(out_of_scope)
743+
finding.duplicate = bool(duplicate)
744+
finding.under_review = False
745+
finding.last_reviewed = finding.mitigated
746+
finding.last_reviewed_by = user
747+
748+
# Create note if provided
749+
new_note = None
750+
if note_entry:
751+
new_note = Notes.objects.create(
752+
entry=note_entry,
753+
author=user,
754+
note_type=note_type,
755+
date=finding.mitigated,
756+
)
757+
finding.notes.add(new_note)
758+
759+
# Endpoint statuses
760+
for status in finding.status_finding.all():
761+
status.mitigated_by = finding.mitigated_by
762+
status.mitigated_time = finding.mitigated
763+
status.mitigated = True
764+
status.last_modified = timezone.now()
765+
status.save()
766+
767+
# Risk acceptance
768+
ra_helper.risk_unaccept(user, finding, perform_save=False)
769+
770+
# External issues (best effort)
771+
close_external_issue(finding, "Closed by defectdojo", "github")
772+
773+
# JIRA sync
774+
push_to_jira = False
775+
finding_in_group = finding.has_finding_group
776+
jira_issue_exists = finding.has_jira_issue or (
777+
finding.finding_group and finding.finding_group.has_jira_issue
778+
)
779+
jira_instance = jira_helper.get_jira_instance(finding)
780+
jira_project = jira_helper.get_jira_project(finding)
781+
if jira_issue_exists:
782+
push_to_jira = (
783+
jira_helper.is_push_all_issues(finding)
784+
or (jira_instance and jira_instance.finding_jira_sync)
785+
)
786+
if new_note and (getattr(jira_project, "push_notes", False) or push_to_jira) and not finding_in_group:
787+
jira_helper.add_comment(finding, new_note, force_push=True)
788+
789+
# Persist and push JIRA if applicable
790+
finding.save(push_to_jira=(push_to_jira and not finding_in_group))
791+
if push_to_jira and finding_in_group:
792+
jira_helper.push_to_jira(finding.finding_group)
793+
794+
# Notification
795+
create_notification(
796+
event="finding_closed",
797+
title=f"Closing of {finding.title}",
798+
finding=finding,
799+
description=f'The finding "{finding.title}" was closed by {user}',
800+
url=reverse("view_finding", args=(finding.id,)),
801+
)

0 commit comments

Comments
 (0)