Skip to content

Commit a033d44

Browse files
committed
Enhance JIRA synchronization logic in importers and serializers
- Updated push_to_jira conditions to include sync behavior based on JIRA instance settings. - Refactored JIRA push logic to check for sync status in FindingSerializer and DefaultImporter. - Improved handling of JIRA instance retrieval and sync checks in DefaultReImporter and BaseImporter. - Added support for prefetched JIRA instance in is_keep_in_sync_with_jira function.
1 parent a75d8e5 commit a033d44

7 files changed

Lines changed: 86 additions & 46 deletions

File tree

dojo/api_v2/serializers.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1858,8 +1858,9 @@ def update(self, instance, validated_data):
18581858
for location_ref in locations:
18591859
location_ref.location.associate_with_finding(instance)
18601860

1861-
if push_to_jira:
1862-
jira_helper.push_to_jira(instance)
1861+
if push_to_jira or finding_helper.is_keep_in_sync_with_jira(instance):
1862+
# Push synchronously so that we can see jira errors in real time
1863+
jira_helper.push_to_jira(instance, sync=True)
18631864

18641865
return instance
18651866

dojo/finding/helper.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
do_dedupe_finding_task_internal,
2525
get_finding_models_for_deduplication,
2626
)
27+
from dojo.jira_link.helper import is_keep_in_sync_with_jira
2728
from dojo.location.models import Location
2829
from dojo.location.status import FindingLocationStatus
2930
from dojo.location.utils import save_locations_to_add
@@ -33,6 +34,7 @@
3334
Engagement,
3435
Finding,
3536
Finding_Group,
37+
JIRA_Instance,
3638
Notes,
3739
System_Settings,
3840
Test,
@@ -459,14 +461,24 @@ def post_process_finding_save_internal(finding, dedupe_option=True, rules_option
459461

460462
@dojo_async_task
461463
@app.task
462-
def post_process_findings_batch(finding_ids, *args, dedupe_option=True, rules_option=True, product_grading_option=True,
463-
issue_updater_option=True, push_to_jira=False, user=None, **kwargs):
464+
def post_process_findings_batch(
465+
finding_ids,
466+
*args,
467+
dedupe_option=True,
468+
rules_option=True,
469+
product_grading_option=True,
470+
issue_updater_option=True,
471+
push_to_jira=False,
472+
jira_instance_id=None,
473+
user=None,
474+
**kwargs,
475+
):
464476

465477
logger.debug(
466478
f"post_process_findings_batch called: finding_ids_count={len(finding_ids) if finding_ids else 0}, "
467479
f"args={args}, dedupe_option={dedupe_option}, rules_option={rules_option}, "
468480
f"product_grading_option={product_grading_option}, issue_updater_option={issue_updater_option}, "
469-
f"push_to_jira={push_to_jira}, user={user.id if user else None}, kwargs={kwargs}",
481+
f"push_to_jira={push_to_jira}, jira_instance_id={jira_instance_id}, user={user.id if user else None}, kwargs={kwargs}",
470482
)
471483
if not finding_ids:
472484
return
@@ -502,14 +514,21 @@ def post_process_findings_batch(finding_ids, *args, dedupe_option=True, rules_op
502514
if product_grading_option and system_settings.enable_product_grade:
503515
calculate_grade(findings[0].test.engagement.product.id)
504516

505-
if push_to_jira:
517+
# If we received the ID of a jira instance, then we need to determine the keep in sync behavior
518+
jira_instance = None
519+
if jira_instance_id is not None:
520+
with suppress(JIRA_Instance.DoesNotExist):
521+
jira_instance = JIRA_Instance.objects.get(id=jira_instance_id)
522+
# We dont check if the finding jira sync is applicable quite yet until we can get in the loop
523+
# but this is a way to at least make it that far
524+
if push_to_jira or getattr(jira_instance, "keep_findings_jira_sync", False):
506525
for finding in findings:
507-
if finding.has_jira_issue or not finding.finding_group:
508-
jira_helper.push_to_jira(finding)
509-
else:
510-
jira_helper.push_to_jira(finding.finding_group)
526+
object_to_push = finding if finding.has_jira_issue or not finding.finding_group else finding.finding_group
527+
# Check the push_to_jira flag again to potentially shorty circuit without checking for existing findings
528+
if push_to_jira or is_keep_in_sync_with_jira(object_to_push, prefetched_jira_instance=jira_instance):
529+
jira_helper.push_to_jira(object_to_push)
511530
else:
512-
logger.debug("push_to_jira is False, not ushing to JIRA")
531+
logger.debug("push_to_jira is False, not pushing to JIRA")
513532

514533

515534
@receiver(pre_delete, sender=Finding)

dojo/importers/base_importer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from dojo.importers.endpoint_manager import EndpointManager
1919
from dojo.importers.location_manager import LocationManager
2020
from dojo.importers.options import ImporterOptions
21+
from dojo.jira_link.helper import is_keep_in_sync_with_jira
2122
from dojo.location.models import AbstractLocation, Location
2223
from dojo.models import (
2324
# Import History States
@@ -998,7 +999,7 @@ def mitigate_finding(
998999
# don't try to dedupe findings that we are closing
9991000
finding.save(dedupe_option=False, product_grading_option=product_grading_option)
10001001
else:
1001-
finding.save(dedupe_option=False, push_to_jira=self.push_to_jira, product_grading_option=product_grading_option)
1002+
finding.save(dedupe_option=False, push_to_jira=(self.push_to_jira or is_keep_in_sync_with_jira(finding, prefetched_jira_instance=self.jira_instance)), product_grading_option=product_grading_option)
10021003

10031004
def notify_scan_added(
10041005
self,

dojo/importers/default_importer.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from dojo.finding import helper as finding_helper
1111
from dojo.importers.base_importer import BaseImporter, Parser
1212
from dojo.importers.options import ImporterOptions
13+
from dojo.jira_link.helper import is_keep_in_sync_with_jira
1314
from dojo.models import (
1415
Engagement,
1516
Finding,
@@ -381,9 +382,13 @@ def close_old_findings(
381382
product_grading_option=False,
382383
)
383384
# push finding groups to jira since we only only want to push whole groups
384-
if self.findings_groups_enabled and self.push_to_jira:
385+
# We dont check if the finding jira sync is applicable quite yet until we can get in the loop
386+
# but this is a way to at least make it that far
387+
if self.findings_groups_enabled and (self.push_to_jira or getattr(self.jira_instance, "keep_findings_jira_sync", False)):
385388
for finding_group in {finding.finding_group for finding in old_findings if finding.finding_group is not None}:
386-
jira_helper.push_to_jira(finding_group)
389+
# Check the push_to_jira flag again to potentially shorty circuit without checking for existing findings
390+
if self.push_to_jira or is_keep_in_sync_with_jira(finding_group, prefetched_jira_instance=self.jira_instance):
391+
jira_helper.push_to_jira(finding_group)
387392

388393
# Calculate grade once after all findings have been closed
389394
if old_findings:

dojo/importers/default_reimporter.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
)
1616
from dojo.importers.base_importer import BaseImporter, Parser
1717
from dojo.importers.options import ImporterOptions
18+
from dojo.jira_link.helper import is_keep_in_sync_with_jira
1819
from dojo.location.status import FindingLocationStatus
1920
from dojo.models import (
2021
Development_Environment,
@@ -439,6 +440,7 @@ def process_findings(
439440
product_grading_option=True,
440441
issue_updater_option=True,
441442
push_to_jira=push_to_jira,
443+
jira_instance_id=getattr(self.jira_instance, "id", None),
442444
)
443445

444446
# No chord: tasks are dispatched immediately above per batch
@@ -497,10 +499,13 @@ def close_old_findings(
497499
)
498500
mitigated_findings.append(finding)
499501
# push finding groups to jira since we only only want to push whole groups
500-
if self.findings_groups_enabled and self.push_to_jira:
502+
# We dont check if the finding jira sync is applicable quite yet until we can get in the loop
503+
# but this is a way to at least make it that far
504+
if self.findings_groups_enabled and (self.push_to_jira or getattr(self.jira_instance, "keep_findings_jira_sync", False)):
501505
for finding_group in {finding.finding_group for finding in findings if finding.finding_group is not None}:
502-
jira_helper.push_to_jira(finding_group)
503-
506+
# Check the push_to_jira flag again to potentially shorty circuit without checking for existing findings
507+
if self.push_to_jira or is_keep_in_sync_with_jira(finding_group, prefetched_jira_instance=self.jira_instance):
508+
jira_helper.push_to_jira(finding_group)
504509
# Calculate grade once after all findings have been closed
505510
if mitigated_findings:
506511
perform_product_grading(self.test.engagement.product)
@@ -983,19 +988,24 @@ def process_groups_for_all_findings(
983988
create_finding_groups_for_all_findings=self.create_finding_groups_for_all_findings,
984989
**kwargs,
985990
)
986-
if self.push_to_jira:
987-
if findings[0].finding_group is not None:
988-
jira_helper.push_to_jira(findings[0].finding_group)
989-
else:
990-
jira_helper.push_to_jira(findings[0])
991-
992-
if self.findings_groups_enabled and self.push_to_jira:
991+
# We dont check if the finding jira sync is applicable quite yet until we can get in the loop
992+
# but this is a way to at least make it that far
993+
if self.push_to_jira or getattr(self.jira_instance, "keep_findings_jira_sync", False):
994+
object_to_push = findings[0].finding_group if findings[0].finding_group is not None else findings[0]
995+
# Check the push_to_jira flag again to potentially shorty circuit without checking for existing findings
996+
if self.push_to_jira or is_keep_in_sync_with_jira(object_to_push, prefetched_jira_instance=self.jira_instance):
997+
jira_helper.push_to_jira(object_to_push)
998+
# We dont check if the finding jira sync is applicable quite yet until we can get in the loop
999+
# but this is a way to at least make it that far
1000+
if self.findings_groups_enabled and (self.push_to_jira or getattr(self.jira_instance, "keep_findings_jira_sync", False)):
9931001
for finding_group in {
9941002
finding.finding_group
9951003
for finding in self.reactivated_items + self.unchanged_items
9961004
if finding.finding_group is not None and not finding.is_mitigated
9971005
}:
998-
jira_helper.push_to_jira(finding_group)
1006+
# Check the push_to_jira flag again to potentially shorty circuit without checking for existing findings
1007+
if self.push_to_jira or is_keep_in_sync_with_jira(finding_group, prefetched_jira_instance=self.jira_instance):
1008+
jira_helper.push_to_jira(finding_group)
9991009

10001010
def process_results(
10011011
self,

dojo/importers/options.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
from django.utils import timezone
1111
from django.utils.functional import SimpleLazyObject
1212

13+
from dojo.jira_link.helper import get_jira_instance
1314
from dojo.models import (
1415
Development_Environment,
1516
Dojo_User,
1617
Endpoint,
1718
Engagement,
1819
Finding,
20+
JIRA_Instance,
1921
Product_API_Scan_Configuration,
2022
Test,
2123
Test_Import,
@@ -70,7 +72,6 @@ def load_base_options(
7072
self.lead: Dojo_User | None = self.validate_lead(*args, **kwargs)
7173
self.minimum_severity: str = self.validate_minimum_severity(*args, **kwargs)
7274
self.parsed_findings: list[Finding] | None = self.validate_parsed_findings(*args, **kwargs)
73-
self.push_to_jira: bool = self.validate_push_to_jira(*args, **kwargs)
7475
self.scan_date: datetime = self.validate_scan_date(*args, **kwargs)
7576
self.scan_type: str = self.validate_scan_type(*args, **kwargs)
7677
self.service: str = self.validate_service(*args, **kwargs)
@@ -80,6 +81,8 @@ def load_base_options(
8081
self.test_title: str = self.validate_test_title(*args, **kwargs)
8182
self.verified: bool = self.validate_verified(*args, **kwargs)
8283
self.version: str = self.validate_version(*args, **kwargs)
84+
# Save this for last to use engagement and test for prefetching related to Jira info
85+
self.push_to_jira: bool = self.validate_push_to_jira(*args, **kwargs)
8386

8487
def load_additional_options(
8588
self,
@@ -478,6 +481,7 @@ def validate_push_to_jira(
478481
*args: list,
479482
**kwargs: dict,
480483
) -> bool:
484+
self.jira_instance: JIRA_Instance | None = get_jira_instance(self.engagement or self.test)
481485
return self.validate(
482486
"push_to_jira",
483487
expected_types=[bool],

dojo/jira_link/helper.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,13 @@ def _safely_get_obj_status_for_jira(obj: Finding | Finding_Group, *, isenforced:
145145
return status or ["Inactive"]
146146

147147

148-
def is_keep_in_sync_with_jira(finding):
148+
def is_keep_in_sync_with_jira(finding, prefetched_jira_instance: JIRA_Instance = None):
149149
keep_in_sync_enabled = False
150150
# Check if there is a jira issue that needs to be updated
151151
jira_issue_exists = finding.has_jira_issue or (finding.finding_group and finding.finding_group.has_jira_issue)
152152
if jira_issue_exists:
153153
# Determine if any automatic sync should occur
154-
jira_instance = get_jira_instance(finding)
154+
jira_instance = prefetched_jira_instance or get_jira_instance(finding)
155155
if jira_instance:
156156
keep_in_sync_enabled = jira_instance.finding_jira_sync
157157

@@ -225,8 +225,8 @@ def can_be_pushed_to_jira(obj, form=None):
225225

226226

227227
# use_inheritance=True means get jira_project config from product if engagement itself has none
228-
def get_jira_project(obj, *, use_inheritance=True):
229-
if not is_jira_enabled():
228+
def get_jira_project(obj, *, use_inheritance=True, jira_enabled: bool = False):
229+
if not jira_enabled and not (jira_enabled := is_jira_enabled()):
230230
return None
231231

232232
if obj is None:
@@ -242,19 +242,19 @@ def get_jira_project(obj, *, use_inheritance=True):
242242
return obj.jira_project
243243
# some old jira_issue records don't have a jira_project, so try to go via the finding instead
244244
if (hasattr(obj, "finding") and obj.finding) or (hasattr(obj, "engagement") and obj.engagement):
245-
return get_jira_project(obj.finding, use_inheritance=use_inheritance)
245+
return get_jira_project(obj.finding, use_inheritance=use_inheritance, jira_enabled=jira_enabled)
246246
return None
247247

248248
if isinstance(obj, Finding | Stub_Finding):
249249
finding = obj
250-
return get_jira_project(finding.test)
250+
return get_jira_project(finding.test, jira_enabled=jira_enabled)
251251

252252
if isinstance(obj, Finding_Group):
253-
return get_jira_project(obj.test)
253+
return get_jira_project(obj.test, jira_enabled=jira_enabled)
254254

255255
if isinstance(obj, Test):
256256
test = obj
257-
return get_jira_project(test.engagement)
257+
return get_jira_project(test.engagement, jira_enabled=jira_enabled)
258258

259259
if isinstance(obj, Engagement):
260260
engagement = obj
@@ -269,7 +269,7 @@ def get_jira_project(obj, *, use_inheritance=True):
269269

270270
if use_inheritance:
271271
logger.debug("delegating to product %s for %s", engagement.product, engagement)
272-
return get_jira_project(engagement.product)
272+
return get_jira_project(engagement.product, jira_enabled=jira_enabled)
273273
logger.debug("not delegating to product %s for %s", engagement.product, engagement)
274274
return None
275275

@@ -286,11 +286,11 @@ def get_jira_project(obj, *, use_inheritance=True):
286286
return None
287287

288288

289-
def get_jira_instance(obj):
290-
if not is_jira_enabled():
289+
def get_jira_instance(obj, jira_enabled: bool = False): # noqa: FBT001, FBT002
290+
if not jira_enabled and not (jira_enabled := is_jira_enabled()):
291291
return None
292292

293-
jira_project = get_jira_project(obj)
293+
jira_project = get_jira_project(obj, jira_enabled=jira_enabled)
294294
if jira_project:
295295
logger.debug("found jira_instance %s for %s", jira_project.jira_instance, obj)
296296
return jira_project.jira_instance
@@ -415,17 +415,17 @@ def get_jira_finding_text(jira_instance):
415415
return None
416416

417417

418-
def has_jira_issue(obj):
418+
def has_jira_issue(obj: Finding | Engagement | Finding_Group) -> bool:
419419
return get_jira_issue(obj) is not None
420420

421421

422-
def get_jira_issue(obj):
423-
if isinstance(obj, Finding | Engagement | Finding_Group):
424-
try:
425-
return obj.jira_issue
426-
except JIRA_Issue.DoesNotExist:
427-
return None
428-
return None
422+
def get_jira_issue(obj: Finding | Engagement | Finding_Group) -> JIRA_Issue | None:
423+
"""
424+
This pattern is "cheaper" than the try/catch handling of the DoesNotExist exception
425+
that would happen if we try to access obj.jira_issue when there is none, and it also
426+
works with prefetch_related where the related object is None instead of a RelatedManager
427+
"""
428+
return getattr(obj, "jira_issue", None)
429429

430430

431431
def has_jira_configured(obj):

0 commit comments

Comments
 (0)