Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions dojo/finding/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,12 @@ def post_process_findings_batch(
@receiver(pre_delete, sender=Finding)
def finding_pre_delete(sender, instance, **kwargs):
logger.debug("finding pre_delete: %d", instance.id)
if (
instance.has_jira_issue
and not getattr(instance, "_skip_jira_close_on_delete", False)
and jira_services.is_delete_sync_allowed(instance)
):
jira_services.close_issue_for_deleted_finding(instance)
# this shouldn't be necessary as Django should remove any Many-To-Many entries automatically, might be a bug in Django?
# https://code.djangoproject.com/ticket/154
instance.found_by.clear()
Expand Down Expand Up @@ -562,7 +568,8 @@ def finding_delete(instance, **kwargs):
if settings.DUPLICATE_CLUSTER_CASCADE_DELETE:
duplicate_cluster.order_by("-id").delete()
else:
reconfigure_duplicate_cluster(instance, duplicate_cluster)
new_original = reconfigure_duplicate_cluster(instance, duplicate_cluster)
_reassign_jira_issue_to_new_original(instance, new_original)
else:
logger.debug("no duplicate cluster found for finding: %d, so no need to reconfigure", instance.id)

Expand All @@ -579,19 +586,41 @@ def finding_post_delete(sender, instance, **kwargs):
logger.debug("finding post_delete, sender: %s instance: %s", to_str_typed(sender), to_str_typed(instance))


def _reassign_jira_issue_to_new_original(deleted_finding, new_original):
if not new_original or new_original.has_jira_issue or not jira_services.is_delete_sync_allowed(deleted_finding):
return False

jira_issue = jira_services.get_issue(deleted_finding)
if not jira_issue:
return False

jira_instance = jira_services.get_instance(deleted_finding)
jira_services.add_simple_comment(
jira_instance,
jira_issue,
(
f"DefectDojo finding {deleted_finding.id} was deleted. "
f"This Jira issue was reassigned to finding {new_original.id}."
),
)
jira_services.reassign_issue_to_finding(jira_issue, new_original)
deleted_finding._skip_jira_close_on_delete = True
return True


# can't use model to id here due to the queryset
# @dojo_async_task
# @app.task
def reconfigure_duplicate_cluster(original, cluster_outside):
# when a finding is deleted, and is an original of a duplicate cluster, we have to chose a new original for the cluster
# only look for a new original if there is one outside this test
if original is None or cluster_outside is None or len(cluster_outside) == 0:
return
return None

if settings.DUPLICATE_CLUSTER_CASCADE_DELETE:
# Don't delete here — the caller (async_delete_crawl_task or finding_delete)
# handles deletion of outside-scope duplicates efficiently via bulk_delete_findings.
return
return None
logger.debug("reconfigure_duplicate_cluster: cluster_outside: %s", cluster_outside)
# set new original to first finding in cluster (ordered by id)
new_original = cluster_outside.order_by("id").first()
Expand All @@ -610,6 +639,8 @@ def reconfigure_duplicate_cluster(original, cluster_outside):

# Re-point remaining duplicates to the new original in a single query
cluster_outside.exclude(id=new_original.id).update(duplicate_finding=new_original)
return new_original
return None


def prepare_duplicates_for_delete(obj, *, preview_only=False):
Expand Down
79 changes: 79 additions & 0 deletions dojo/jira/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ def is_keep_in_sync_with_jira(obj: Finding | Finding_Group, prefetched_jira_inst
return False


def is_delete_sync_allowed(finding):
return bool(is_keep_in_sync_with_jira(finding) or is_push_all_issues(finding))


# checks if a finding can be pushed to JIRA
# optionally provides a form with the new data for the finding
# any finding that already has a JIRA issue can be pushed again to JIRA
Expand Down Expand Up @@ -1268,6 +1272,81 @@ def push_status_to_jira(obj, jira_instance, jira, issue, *, save=False):
return updated


def close_jira_issue_for_deleted_finding(finding) -> tuple[bool | None, str]:
logger.debug("closing linked Jira issue before deleting finding %d", finding.id)

if not is_jira_enabled():
return False, "JIRA integration is not enabled."

if not finding.has_jira_issue:
return False, f"Finding {finding.id} has no linked JIRA issue."

if not is_delete_sync_allowed(finding):
return False, f"Finding {finding.id} is not configured to sync deleted findings to JIRA."

if not is_jira_configured_and_enabled(finding):
message = (
f"Finding {finding.id} cannot close its linked JIRA issue "
"because JIRA is not configured or enabled."
)
logger.debug(message)
return False, message

jira_instance = get_jira_instance(finding)
if not jira_instance:
message = (
f"Finding {finding.id} cannot close its linked JIRA issue "
"because the JIRA instance is not available."
)
logger.warning(message)
return False, message

jira_issue = get_jira_issue(finding)
if not jira_issue:
return False, f"Finding {finding.id} has no local JIRA issue record."

try:
JIRAError.log_to_tempfile = False
jira = get_jira_connection(jira_instance)
if not jira:
message = (
f"Finding {finding.id} cannot close its linked JIRA issue "
"because the JIRA connection could not be established."
)
logger.warning(message)
return False, message
issue = jira.issue(jira_issue.jira_id)
except Exception as e:
message = f"The following jira instance could not be connected: {jira_instance} - {e}"
logger.exception(message)
log_jira_alert(message, finding)
return False, message

if not issue_from_jira_is_active(issue):
logger.debug("Jira issue %s is already resolved", jira_issue.jira_key)
return False, f"Jira issue {jira_issue.jira_key} is already resolved."

updated = jira_transition(jira, issue, jira_instance.close_status_key)
if updated:
add_simple_jira_comment(
jira_instance,
jira_issue,
f"DefectDojo finding {finding.id} was deleted. This Jira issue was closed automatically.",
)
jira_issue.jira_change = timezone.now()
jira_issue.save(update_fields=["jira_change"])
return True, f"Jira issue {jira_issue.jira_key} closed successfully."

return updated, f"Jira issue {jira_issue.jira_key} was not closed."


def reassign_jira_issue_to_finding(jira_issue, finding):
jira_issue.finding = finding
jira_issue.finding_group = None
jira_issue.engagement = None
jira_issue.save(update_fields=["finding", "finding_group", "engagement"])


# gets the metadata for the provided issue type in the provided jira project
def get_issuetype_fields(
jira,
Expand Down
27 changes: 27 additions & 0 deletions dojo/jira/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,24 @@ def push_status(obj, jira_instance, jira, issue, *, save=False):
return _get_helper().push_status_to_jira(obj, jira_instance, jira, issue, save=save)


def close_issue_for_deleted_finding(finding):
"""
Close the linked Jira issue before a finding is deleted.

Wraps: jira_helper.close_jira_issue_for_deleted_finding
"""
return _get_helper().close_jira_issue_for_deleted_finding(finding)


def reassign_issue_to_finding(jira_issue, finding):
"""
Reassign a local Jira issue record to another finding.

Wraps: jira_helper.reassign_jira_issue_to_finding
"""
return _get_helper().reassign_jira_issue_to_finding(jira_issue, finding)


def update_issue(obj, *args, **kwargs):
"""
Update a Jira issue.
Expand Down Expand Up @@ -339,6 +357,15 @@ def is_keep_in_sync(obj, prefetched_jira_instance=None):
return _get_helper().is_keep_in_sync_with_jira(obj, prefetched_jira_instance=prefetched_jira_instance)


def is_delete_sync_allowed(finding):
"""
Check if deleting a finding should update its linked Jira issue.

Wraps: jira_helper.is_delete_sync_allowed
"""
return _get_helper().is_delete_sync_allowed(finding)


def is_push(instance, push_to_jira_parameter=None):
"""
Check if Jira push should happen.
Expand Down
Loading
Loading