Skip to content

Commit a7aa119

Browse files
authored
Fix Jira integration error handling and type representation (#14320)
* fix: update to_str_typed function to use __name__ for type representation * fix: handle errors when pushing findings to Jira in FindingSerializer * refactor: update push_to_jira and related functions to return status and messages * fix: prevent request failure by skipping JIRA push when not configured in test case
1 parent f414696 commit a7aa119

4 files changed

Lines changed: 71 additions & 57 deletions

File tree

dojo/api_v2/serializers.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1890,7 +1890,9 @@ def update(self, instance, validated_data):
18901890

18911891
if push_to_jira or finding_helper.is_keep_in_sync_with_jira(instance):
18921892
# Push synchronously so that we can see jira errors in real time
1893-
jira_helper.push_to_jira(instance, sync=True)
1893+
success, message = jira_helper.push_to_jira(instance, sync=True)
1894+
if not success:
1895+
raise serializers.ValidationError(message)
18941896

18951897
return instance
18961898

dojo/jira_link/helper.py

Lines changed: 65 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,7 @@ def jira_environment(obj):
757757
return ""
758758

759759

760-
def push_to_jira(obj, *args, **kwargs):
760+
def push_to_jira(obj, *args, **kwargs) -> tuple[str, bool]:
761761
if obj is None:
762762
msg = "Cannot push None to JIRA"
763763
raise ValueError(msg)
@@ -773,29 +773,32 @@ def push_to_jira(obj, *args, **kwargs):
773773

774774
if isinstance(obj, Engagement):
775775
return dojo_dispatch_task(push_engagement_to_jira, obj.id, *args, **kwargs)
776-
logger.error("unsupported object passed to push_to_jira: %s %i %s", obj.__name__, obj.id, obj)
777-
return None
776+
message = f"unsupported object passed to push_to_jira: {obj.__class__.__name__} {obj.id} {obj}"
777+
logger.error(message)
778+
return False, message
778779

779780

780781
# we need thre separate celery tasks due to the decorators we're using to map to/from ids
781782
@app.task
782-
def push_finding_to_jira(finding_id, *args, **kwargs):
783+
def push_finding_to_jira(finding_id, *args, **kwargs) -> tuple[str, bool]:
783784
finding = get_object_or_none(Finding, id=finding_id)
784785
if not finding:
785-
logger.warning("Finding with id %s does not exist, skipping push_finding_to_jira", finding_id)
786-
return None
786+
message = f"Finding with id {finding_id} does not exist, skipping push_finding_to_jira"
787+
logger.warning(message)
788+
return False, message
787789

788790
if finding.has_jira_issue:
789791
return update_jira_issue(finding, *args, **kwargs)
790792
return add_jira_issue(finding, *args, **kwargs)
791793

792794

793795
@app.task
794-
def push_finding_group_to_jira(finding_group_id, *args, **kwargs):
796+
def push_finding_group_to_jira(finding_group_id, *args, **kwargs) -> tuple[str, bool]:
795797
finding_group = get_object_or_none(Finding_Group, id=finding_group_id)
796798
if not finding_group:
797-
logger.warning("Finding_Group with id %s does not exist, skipping push_finding_group_to_jira", finding_group_id)
798-
return None
799+
message = f"Finding_Group with id {finding_group_id} does not exist, skipping push_finding_group_to_jira"
800+
logger.warning(message)
801+
return False, message
799802

800803
# Look for findings that have single ticket associations separate from the group
801804
for finding in finding_group.findings.filter(jira_issue__isnull=False):
@@ -807,11 +810,12 @@ def push_finding_group_to_jira(finding_group_id, *args, **kwargs):
807810

808811

809812
@app.task
810-
def push_engagement_to_jira(engagement_id, *args, **kwargs):
813+
def push_engagement_to_jira(engagement_id, *args, **kwargs) -> tuple[str, bool]:
811814
engagement = get_object_or_none(Engagement, id=engagement_id)
812815
if not engagement:
813-
logger.warning("Engagement with id %s does not exist, skipping push_engagement_to_jira", engagement_id)
814-
return None
816+
message = f"Engagement with id {engagement_id} does not exist, skipping push_engagement_to_jira"
817+
logger.warning(message)
818+
return False, message
815819

816820
if engagement.has_jira_issue:
817821
return dojo_dispatch_task(update_epic, engagement.id, *args, **kwargs)
@@ -894,18 +898,18 @@ def prepare_jira_issue_fields(
894898
return fields
895899

896900

897-
def add_jira_issue(obj, *args, **kwargs):
898-
def failure_to_add_message(message: str, exception: Exception, _: Any) -> bool:
901+
def add_jira_issue(obj, *args, **kwargs) -> tuple[str, bool]:
902+
def failure_to_add_message(message: str, exception: Exception, _: Any) -> tuple[str, bool]:
899903
if exception:
900904
logger.error("Exception occurred", exc_info=exception)
901905
logger.error(message)
902906
log_jira_alert(message, obj)
903-
return False
907+
return False, message
904908

905909
logger.info("trying to create a new jira issue for %d:%s", obj.id, to_str_typed(obj))
906910

907911
if not is_jira_enabled():
908-
return False
912+
return False, "JIRA integration is not enabled."
909913

910914
if not is_jira_configured_and_enabled(obj):
911915
message = f"Object {obj.id} cannot be pushed to JIRA as there is no JIRA configuration for {to_str_typed(obj)}."
@@ -932,20 +936,20 @@ def failure_to_add_message(message: str, exception: Exception, _: Any) -> bool:
932936

933937
# not sure why this check is not part of can_be_pushed_to_jira, but afraid to change it
934938
if isinstance(obj, Finding) and obj.duplicate and not obj.active:
935-
logger.info("%s will not be pushed to JIRA as it's a duplicate finding", to_str_typed(obj))
939+
message = f"{to_str_typed(obj)} is a duplicate and inactive finding, and will not be pushed to JIRA: {error_message}."
936940
# Duplicates are expected, don't create alerts
937-
logger.info("%s cannot be pushed to JIRA: %s (expected - duplicate finding)",
938-
to_str_typed(obj), error_message)
941+
logger.info(message)
939942
elif error_code in expected_validation_errors:
943+
message = f"{to_str_typed(obj)} cannot be pushed to JIRA: {error_message}."
940944
# These are expected when auto-pushing, only log, don't alert
941-
logger.info("%s cannot be pushed to JIRA: %s (expected - finding not ready yet)",
942-
to_str_typed(obj), error_message)
945+
logger.info(message)
943946
else:
944947
# Unexpected errors (configuration issues, etc.) should still alert
945-
log_jira_cannot_be_pushed_reason(error_message, obj)
946-
logger.warning("%s cannot be pushed to JIRA: %s.", to_str_typed(obj), error_message)
948+
message = f"{to_str_typed(obj)} cannot be pushed to JIRA due to an unexpected error: {error_message}."
949+
log_jira_cannot_be_pushed_reason(message, obj)
950+
logger.warning("%s cannot be pushed to JIRA: %s.", to_str_typed(obj), message)
947951
logger.warning("The JIRA issue will NOT be created.")
948-
return False
952+
return False, message
949953
logger.debug("Trying to create a new JIRA issue for %s...", to_str_typed(obj))
950954
# Attempt to get the jira connection
951955
try:
@@ -1056,21 +1060,21 @@ def failure_to_add_message(message: str, exception: Exception, _: Any) -> bool:
10561060
message = f"Failed to assign jira issue to existing epic: {e}"
10571061
return failure_to_add_message(message, e, obj)
10581062

1059-
return True
1063+
return True, "JIRA issue created successfully."
10601064

10611065

1062-
def update_jira_issue(obj, *args, **kwargs):
1063-
def failure_to_update_message(message: str, exception: Exception, obj: Any) -> bool:
1066+
def update_jira_issue(obj, *args, **kwargs) -> tuple[str, bool]:
1067+
def failure_to_update_message(message: str, exception: Exception, obj: Any) -> tuple[str, bool]:
10641068
if exception:
10651069
logger.error(exception)
10661070
logger.error(message)
10671071
log_jira_alert(message, obj)
1068-
return False
1072+
return False, message
10691073

10701074
logger.debug("trying to update a linked jira issue for %d:%s", obj.id, to_str_typed(obj))
10711075

10721076
if not is_jira_enabled():
1073-
return False
1077+
return False, "JIRA integration is not enabled."
10741078

10751079
jira_project = get_jira_project(obj)
10761080
jira_instance = get_jira_instance(obj)
@@ -1181,7 +1185,7 @@ def failure_to_update_message(message: str, exception: Exception, obj: Any) -> b
11811185
message = f"Failed to assign jira issue to existing epic: {e}"
11821186
return failure_to_update_message(message, e, obj)
11831187

1184-
return True
1188+
return True, "JIRA issue updated successfully."
11851189

11861190

11871191
def get_jira_issue_from_jira(find):
@@ -1447,13 +1451,14 @@ def close_epic(engagement_id, push_to_jira, **kwargs):
14471451
def update_epic(engagement_id, **kwargs):
14481452
engagement = get_object_or_none(Engagement, id=engagement_id)
14491453
if not engagement:
1450-
logger.warning("Engagement with id %s does not exist, skipping update_epic", engagement_id)
1451-
return False
1454+
message = f"Engagement with id {engagement_id} does not exist, skipping update_epic"
1455+
logger.warning(message)
1456+
return False, message
14521457

14531458
logger.debug("trying to update jira EPIC for %d:%s", engagement.id, engagement.name)
14541459

14551460
if not is_jira_configured_and_enabled(engagement):
1456-
return False
1461+
return False, "JIRA integration is not properly configured for this engagement."
14571462

14581463
logger.debug("config found")
14591464

@@ -1479,35 +1484,40 @@ def update_epic(engagement_id, **kwargs):
14791484
jira_issue_update_kwargs["priority"] = {"name": epic_priority}
14801485
issue.update(**jira_issue_update_kwargs)
14811486
except JIRAError as e:
1482-
logger.exception("Jira Engagement/Epic Update Error")
1483-
log_jira_generic_alert("Jira Engagement/Epic Update Error", str(e))
1484-
return False
1487+
message = f"Failed to update the JIRA EPIC for engagement {engagement.id} - {e}"
1488+
logger.exception(message)
1489+
log_jira_generic_alert(message)
1490+
return False, message
14851491

1486-
return True
1492+
return True, "JIRA EPIC updated successfully."
14871493

1488-
add_error_message_to_response("Push to JIRA for Epic skipped because enable_engagement_epic_mapping is not checked for this engagement")
1489-
return False
1494+
message = f"Push to JIRA for Epic skipped because enable_engagement_epic_mapping is not checked for engagement {engagement.id}."
1495+
add_error_message_to_response(message)
1496+
return False, message
14901497

14911498

14921499
@app.task
14931500
def add_epic(engagement_id, **kwargs):
14941501
engagement = get_object_or_none(Engagement, id=engagement_id)
14951502
if not engagement:
1496-
logger.warning("Engagement with id %s does not exist, skipping add_epic", engagement_id)
1497-
return False
1503+
message = f"Engagement with id {engagement_id} does not exist, skipping add_epic"
1504+
logger.warning(message)
1505+
return False, message
14981506

14991507
logger.debug("trying to create a new jira EPIC for %d:%s", engagement.id, engagement.name)
15001508

15011509
if not is_jira_configured_and_enabled(engagement):
1502-
return False
1510+
message = f"JIRA integration is not properly configured for engagement {engagement.id}."
1511+
return False, message
15031512

15041513
logger.debug("config found")
15051514

15061515
jira_project = get_jira_project(engagement)
15071516
jira_instance = get_jira_instance(engagement)
15081517
if not jira_instance:
1509-
logger.warning("JIRA add epic failed: jira_instance is None")
1510-
return False
1518+
message = f"JIRA add epic failed for engagement {engagement.id}: jira_instance is None"
1519+
logger.warning(message)
1520+
return False, message
15111521

15121522
if jira_project and jira_project.enable_engagement_epic_mapping:
15131523
epic_name = kwargs.get("epic_name")
@@ -1555,15 +1565,15 @@ def add_epic(engagement_id, **kwargs):
15551565
message = "The 'Epic name id' in your DefectDojo Jira Configuration does not appear to be correct. Please visit, " + jira_instance.url + \
15561566
"/rest/api/2/field and search for Epic Name. Copy the number out of cf[number] and place in your DefectDojo settings for Jira and try again. For example, if your results are cf[100001] then copy 100001 and place it in 'Epic name id'. (Your Epic Id will be different.) \n\n"
15571567
logger.exception(message)
1568+
message = f"JIRA add epic failed for engagement {engagement.id}: {message}"
1569+
log_jira_generic_alert(message)
1570+
return False, message
15581571

1559-
log_jira_generic_alert("Jira Engagement/Epic Creation Error",
1560-
message + error)
1561-
return False
1562-
1563-
return True
1572+
return True, "JIRA EPIC created successfully."
15641573

1565-
add_error_message_to_response("Push to JIRA for Epic skipped because enable_engagement_epic_mapping is not checked for this engagement")
1566-
return False
1574+
message = f"Push to JIRA for Epic skipped because enable_engagement_epic_mapping is not checked for engagement {engagement.id}."
1575+
add_error_message_to_response(message)
1576+
return False, message
15671577

15681578

15691579
def jira_get_issue(jira_project, issue_key):
@@ -1847,7 +1857,8 @@ def process_jira_epic_form(request, engagement=None):
18471857
epic_priority = None
18481858
if jira_epic_form.cleaned_data.get("epic_priority"):
18491859
epic_priority = jira_epic_form.cleaned_data.get("epic_priority")
1850-
if push_to_jira(engagement, epic_name=epic_name, epic_priority=epic_priority):
1860+
success, message = push_to_jira(engagement, epic_name=epic_name, epic_priority=epic_priority)
1861+
if success:
18511862
logger.debug("Push to JIRA for Epic queued successfully")
18521863
messages.add_message(
18531864
request,
@@ -1856,11 +1867,11 @@ def process_jira_epic_form(request, engagement=None):
18561867
extra_tags="alert-success")
18571868
else:
18581869
error = True
1859-
logger.debug("Push to JIRA for Epic failey")
1870+
logger.debug("Push to JIRA for Epic failed")
18601871
messages.add_message(
18611872
request,
18621873
messages.ERROR,
1863-
"Push to JIRA for Epic failed, check alerts on the top right for errors",
1874+
message,
18641875
extra_tags="alert-danger")
18651876
else:
18661877
logger.debug("invalid jira epic form")

dojo/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1971,7 +1971,7 @@ def mass_model_updater(model_type, models, function, fields, page_size=1000, ord
19711971

19721972
def to_str_typed(obj):
19731973
"""For code that handles multiple types of objects, print not only __str__ but prefix the type of the object"""
1974-
return f"{type(obj)}: {obj}"
1974+
return f"{type(obj).__name__}: {obj}"
19751975

19761976

19771977
def get_product(obj):

unittests/test_rest_framework.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1788,7 +1788,8 @@ def __init__(self, *args, **kwargs):
17881788
"files": [],
17891789
"tags": ["tag1", "tag_2"],
17901790
}
1791-
self.update_fields = {"duplicate": False, "active": True, "push_to_jira": "True", "tags": ["finding_tag_new"]}
1791+
# Do not push to jira here as it will make the request fail due to jira not being configured
1792+
self.update_fields = {"duplicate": False, "active": True, "tags": ["finding_tag_new"]}
17921793
self.test_type = TestType.OBJECT_PERMISSIONS
17931794
self.permission_check_class = Finding
17941795
self.permission_create = Permissions.Finding_Add

0 commit comments

Comments
 (0)