|
7 | 7 | from django.db.models.signals import post_delete, pre_delete |
8 | 8 | from django.db.utils import IntegrityError |
9 | 9 | from django.dispatch.dispatcher import receiver |
| 10 | +from django.urls import reverse |
10 | 11 | from django.utils import timezone |
11 | 12 | from fieldsignals import pre_save_changed |
12 | 13 |
|
13 | 14 | import dojo.jira_link.helper as jira_helper |
| 15 | +import dojo.risk_acceptance.helper as ra_helper |
14 | 16 | from dojo.celery import app |
15 | 17 | from dojo.decorators import dojo_async_task, dojo_model_from_id, dojo_model_to_id |
16 | 18 | from dojo.endpoint.utils import save_endpoints_to_add |
|
21 | 23 | Engagement, |
22 | 24 | Finding, |
23 | 25 | Finding_Group, |
| 26 | + Notes, |
24 | 27 | System_Settings, |
25 | 28 | Test, |
26 | 29 | Vulnerability_Id, |
27 | 30 | Vulnerability_Id_Template, |
28 | 31 | ) |
29 | 32 | from dojo.notes.helper import delete_related_notes |
| 33 | +from dojo.notifications.helper import create_notification |
30 | 34 | from dojo.tools import tool_issue_updater |
31 | 35 | from dojo.utils import ( |
32 | 36 | calculate_grade, |
| 37 | + close_external_issue, |
33 | 38 | do_dedupe_finding, |
34 | 39 | do_false_positive_history, |
35 | 40 | get_current_user, |
@@ -161,6 +166,24 @@ def update_finding_status(new_state_finding, user, changed_fields=None): |
161 | 166 | new_state_finding.last_status_update = now |
162 | 167 |
|
163 | 168 |
|
| 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 | + |
164 | 187 | def can_edit_mitigated_data(user): |
165 | 188 | return settings.EDITABLE_MITIGATED_DATA and user.is_superuser |
166 | 189 |
|
@@ -271,7 +294,6 @@ def get_group_by_group_name(finding, finding_group_by_option): |
271 | 294 | else: |
272 | 295 | msg = f"Invalid group_by option {finding_group_by_option}" |
273 | 296 | raise ValueError(msg) |
274 | | - |
275 | 297 | if group_name: |
276 | 298 | return f"Findings in: {group_name}" |
277 | 299 |
|
@@ -689,3 +711,91 @@ def save_vulnerability_ids_template(finding_template, vulnerability_ids): |
689 | 711 | finding_template.cve = vulnerability_ids[0] |
690 | 712 | else: |
691 | 713 | 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