Skip to content

Commit f50bbfe

Browse files
authored
Jira Webhook: Reorg logging and responses (DefectDojo#10049)
* Jira Webhook: Reorg logging and responses * Remove `get_object_or_404` * Take more control over logging * Update Unit tests * Fix unit tests * Fix typo * Update status code
1 parent fd122cc commit f50bbfe

File tree

2 files changed

+149
-124
lines changed

2 files changed

+149
-124
lines changed

dojo/jira_link/views.py

Lines changed: 137 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@
77
from django.contrib.admin.utils import NestedObjects
88
from django.urls import reverse
99
from django.db import DEFAULT_DB_ALIAS
10-
from django.http import HttpResponseRedirect, HttpResponse, Http404, HttpResponseBadRequest
10+
from django.http import HttpResponseRedirect, HttpResponse, Http404
1111
from django.shortcuts import render, get_object_or_404
1212
from django.utils import timezone
1313
from django.utils.dateparse import parse_datetime
1414
from django.views.decorators.csrf import csrf_exempt
1515
from django.core.exceptions import PermissionDenied
1616
# Local application/library imports
1717
from dojo.forms import JIRAForm, DeleteJIRAInstanceForm, ExpressJIRAForm
18-
from dojo.models import User, JIRA_Instance, JIRA_Issue, Notes
19-
from dojo.utils import add_breadcrumb, add_error_message_to_response, get_system_setting
18+
from dojo.models import System_Settings, User, JIRA_Instance, JIRA_Issue, Notes
19+
from dojo.utils import add_breadcrumb, add_error_message_to_response
2020
from dojo.notifications.helper import create_notification
2121
from django.views.decorators.http import require_POST
2222
import dojo.jira_link.helper as jira_helper
@@ -26,114 +26,140 @@
2626
logger = logging.getLogger(__name__)
2727

2828

29-
# for examples of incoming json, see the unit tests for the webhook: https://github.com/DefectDojo/django-DefectDojo/blob/master/unittests/test_jira_webhook.py
30-
# or the officials docs (which are not always clear): https://developer.atlassian.com/server/jira/platform/webhooks/
29+
def webhook_responser_handler(
30+
log_level: str,
31+
message: str,
32+
) -> HttpResponse:
33+
# These represent an error and will be sent to the debugger
34+
# for development purposes
35+
if log_level == "info":
36+
logger.info(message)
37+
# These are more common in misconfigurations and have a better
38+
# chance of being seen by a user
39+
elif log_level == "debug":
40+
logger.debug(message)
41+
# Return the response with the code
42+
return HttpResponse(message, status=200)
43+
44+
3145
@csrf_exempt
3246
@require_POST
3347
def webhook(request, secret=None):
34-
if not get_system_setting('enable_jira'):
35-
logger.debug('ignoring incoming webhook as JIRA is disabled.')
36-
raise Http404('JIRA disabled')
37-
elif not get_system_setting('enable_jira_web_hook'):
38-
logger.debug('ignoring incoming webhook as JIRA Webhook is disabled.')
39-
raise Http404('JIRA Webhook disabled')
40-
elif not get_system_setting('disable_jira_webhook_secret'):
41-
if not get_system_setting('jira_webhook_secret'):
42-
logger.warning('ignoring incoming webhook as JIRA Webhook secret is empty in Defect Dojo system settings.')
43-
raise PermissionDenied('JIRA Webhook secret cannot be empty')
44-
if secret != get_system_setting('jira_webhook_secret'):
45-
logger.warning('invalid secret provided to JIRA Webhook')
46-
raise PermissionDenied('invalid or no secret provided to JIRA Webhook')
48+
"""
49+
for examples of incoming json, see the unit tests for the webhook:
50+
https://github.com/DefectDojo/django-DefectDojo/blob/master/unittests/test_jira_webhook.py
51+
or the officials docs (which are not always clear):
52+
https://developer.atlassian.com/server/jira/platform/webhooks/
4753
54+
All responses here will return a 201 so that we may have control over the
55+
logging level
56+
"""
57+
# Make sure the request is a POST, otherwise, we reject
58+
if request.method != "POST":
59+
return webhook_responser_handler("debug", "Only POST requests are supported")
60+
# Determine if th webhook is in use or not
61+
system_settings = System_Settings.objects.get()
62+
# If the jira integration is not enabled, then return a 404
63+
if not system_settings.enable_jira:
64+
return webhook_responser_handler("info", "Ignoring incoming webhook as JIRA is disabled.")
65+
# If the webhook is not enabled, then return a 404
66+
elif not system_settings.enable_jira_web_hook:
67+
return webhook_responser_handler("info", "Ignoring incoming webhook as JIRA Webhook is disabled.")
68+
# Determine if the request should be "authenticated"
69+
elif not system_settings.disable_jira_webhook_secret:
70+
# Make sure there is a value for the webhook secret before making a comparison
71+
if not system_settings.jira_webhook_secret:
72+
return webhook_responser_handler("info", "Ignoring incoming webhook as JIRA Webhook secret is empty in Defect Dojo system settings.")
73+
# Make sure the secret supplied in the path of the webhook request matches the
74+
# secret supplied in the system settings
75+
if secret != system_settings.jira_webhook_secret:
76+
return webhook_responser_handler("info", "Invalid or no secret provided to JIRA Webhook")
4877
# if webhook secret is disabled in system_settings, we ignore the incoming secret, even if it doesn't match
49-
5078
# example json bodies at the end of this file
51-
52-
if request.content_type != 'application/json':
53-
return HttpResponseBadRequest("only application/json supported")
54-
55-
if request.method == 'POST':
56-
try:
57-
parsed = json.loads(request.body.decode('utf-8'))
58-
if parsed.get('webhookEvent') == 'jira:issue_updated':
59-
# xml examples at the end of file
60-
jid = parsed['issue']['id']
61-
jissue = get_object_or_404(JIRA_Issue, jira_id=jid)
62-
63-
findings = None
64-
if jissue.finding:
65-
logging.info(f"Received issue update for {jissue.jira_key} for finding {jissue.finding.id}")
66-
findings = [jissue.finding]
67-
elif jissue.finding_group:
68-
logging.info(f"Received issue update for {jissue.jira_key} for finding group {jissue.finding_group}")
69-
findings = jissue.finding_group.findings.all()
70-
elif jissue.engagement:
71-
# if parsed['issue']['fields']['resolution'] != None:
72-
# eng.active = False
73-
# eng.status = 'Completed'
74-
# eng.save()
75-
return HttpResponse('Update for engagement ignored')
76-
else:
77-
logging.info(f"Received issue update for {jissue.jira_key} for unknown object")
78-
raise Http404(f'No finding, finding_group or engagement found for JIRA issue {jissue.jira_key}')
79-
80-
assignee = parsed['issue']['fields'].get('assignee')
81-
assignee_name = 'Jira User'
82-
if assignee is not None:
83-
# First look for the 'name' field. If not present, try 'displayName'. Else put None
84-
assignee_name = assignee.get('name', assignee.get('displayName'))
85-
86-
resolution = parsed['issue']['fields']['resolution']
87-
88-
# "resolution":{
89-
# "self":"http://www.testjira.com/rest/api/2/resolution/11",
90-
# "id":"11",
91-
# "description":"Cancelled by the customer.",
92-
# "name":"Cancelled"
93-
# },
94-
95-
# or
96-
# "resolution": null
97-
98-
# or
99-
# "resolution": "None"
100-
101-
resolution = resolution if resolution and resolution != "None" else None
102-
resolution_id = resolution['id'] if resolution else None
103-
resolution_name = resolution['name'] if resolution else None
104-
jira_now = parse_datetime(parsed['issue']['fields']['updated'])
105-
106-
if findings:
107-
for finding in findings:
108-
jira_helper.process_resolution_from_jira(finding, resolution_id, resolution_name, assignee_name, jira_now, jissue)
109-
# Check for any comment that could have come along with the resolution
110-
if (error_response := check_for_and_create_comment(parsed)) is not None:
111-
return error_response
112-
113-
if parsed.get('webhookEvent') == 'comment_created':
114-
if (error_response := check_for_and_create_comment(parsed)) is not None:
115-
return error_response
116-
117-
if parsed.get('webhookEvent') not in ['comment_created', 'jira:issue_updated']:
118-
logger.info(f"Unrecognized JIRA webhook event received: {parsed.get('webhookEvent')}")
119-
120-
except Exception as e:
121-
if isinstance(e, Http404):
122-
logger.warning('404 error processing JIRA webhook')
123-
logger.warning(str(e))
124-
else:
125-
logger.exception(e)
126-
79+
if request.content_type != "application/json":
80+
return webhook_responser_handler("debug", "only application/json supported")
81+
# Time to process the request
82+
try:
83+
parsed = json.loads(request.body.decode("utf-8"))
84+
# Check if the events supplied are supported
85+
if parsed.get('webhookEvent') not in ['comment_created', 'jira:issue_updated']:
86+
return webhook_responser_handler("info", f"Unrecognized JIRA webhook event received: {parsed.get('webhookEvent')}")
87+
88+
if parsed.get('webhookEvent') == 'jira:issue_updated':
89+
# xml examples at the end of file
90+
jid = parsed['issue']['id']
91+
# This may raise a 404, but it will be handled in the exception response
12792
try:
128-
logger.debug('jira_webhook_body_parsed:')
129-
logger.debug(json.dumps(parsed, indent=4))
130-
except Exception:
131-
logger.debug('jira_webhook_body:')
132-
logger.debug(request.body.decode('utf-8'))
93+
jissue = JIRA_Issue.objects.get(jira_id=jid)
94+
except JIRA_Instance.DoesNotExist:
95+
return webhook_responser_handler("info", f"JIRA issue {jid} is not linked to a DefectDojo Finding")
96+
findings = None
97+
# Determine what type of object we will be working with
98+
if jissue.finding:
99+
logging.debug(f"Received issue update for {jissue.jira_key} for finding {jissue.finding.id}")
100+
findings = [jissue.finding]
101+
elif jissue.finding_group:
102+
logging.debug(f"Received issue update for {jissue.jira_key} for finding group {jissue.finding_group}")
103+
findings = jissue.finding_group.findings.all()
104+
elif jissue.engagement:
105+
return webhook_responser_handler("debug", "Update for engagement ignored")
106+
else:
107+
return webhook_responser_handler("info", f"Received issue update for {jissue.jira_key} for unknown object")
108+
# Process the assignee if present
109+
assignee = parsed['issue']['fields'].get('assignee')
110+
assignee_name = 'Jira User'
111+
if assignee is not None:
112+
# First look for the 'name' field. If not present, try 'displayName'. Else put None
113+
assignee_name = assignee.get('name', assignee.get('displayName'))
114+
115+
# "resolution":{
116+
# "self":"http://www.testjira.com/rest/api/2/resolution/11",
117+
# "id":"11",
118+
# "description":"Cancelled by the customer.",
119+
# "name":"Cancelled"
120+
# },
121+
122+
# or
123+
# "resolution": null
124+
125+
# or
126+
# "resolution": "None"
127+
128+
resolution = parsed['issue']['fields']['resolution']
129+
resolution = resolution if resolution and resolution != "None" else None
130+
resolution_id = resolution['id'] if resolution else None
131+
resolution_name = resolution['name'] if resolution else None
132+
jira_now = parse_datetime(parsed['issue']['fields']['updated'])
133+
134+
if findings:
135+
for finding in findings:
136+
jira_helper.process_resolution_from_jira(finding, resolution_id, resolution_name, assignee_name, jira_now, jissue)
137+
# Check for any comment that could have come along with the resolution
138+
if (error_response := check_for_and_create_comment(parsed)) is not None:
139+
return error_response
140+
141+
if parsed.get('webhookEvent') == 'comment_created':
142+
if (error_response := check_for_and_create_comment(parsed)) is not None:
143+
return error_response
144+
145+
except Exception as e:
146+
# Check if the issue is originally a 404
147+
if isinstance(e, Http404):
148+
return webhook_responser_handler("debug", str(e))
149+
# Try to get a little more information on the exact exception
150+
try:
151+
message = (
152+
f"Original Exception: {e}\n"
153+
f"jira webhook body parsed:\n{json.dumps(parsed, indent=4)}"
154+
)
155+
except Exception:
156+
message = (
157+
f"Original Exception: {e}\n"
158+
f"jira webhook body :\n{request.body.decode('utf-8')}"
159+
)
160+
return webhook_responser_handler("debug", message)
133161

134-
# reraise to make sure we don't silently swallow things
135-
raise
136-
return HttpResponse('')
162+
return webhook_responser_handler("No logging here", "Success!")
137163

138164

139165
def check_for_and_create_comment(parsed_json):
@@ -194,31 +220,30 @@ def check_for_and_create_comment(parsed_json):
194220
commenter_display_name = comment.get('updateAuthor', {}).get('displayName')
195221
# example: body['comment']['self'] = "http://www.testjira.com/jira_under_a_path/rest/api/2/issue/666/comment/456843"
196222
jid = comment.get('self', '').split('/')[-3]
197-
jissue = get_object_or_404(JIRA_Issue, jira_id=jid)
198-
logging.info(f"Received issue comment for {jissue.jira_key}")
223+
try:
224+
jissue = JIRA_Issue.objects.get(jira_id=jid)
225+
except JIRA_Instance.DoesNotExist:
226+
return webhook_responser_handler("info", f"JIRA issue {jid} is not linked to a DefectDojo Finding")
227+
logging.debug(f"Received issue comment for {jissue.jira_key}")
199228
logger.debug('jissue: %s', vars(jissue))
200229

201230
jira_usernames = JIRA_Instance.objects.values_list('username', flat=True)
202231
for jira_user_id in jira_usernames:
203232
# logger.debug('incoming username: %s jira config username: %s', commenter.lower(), jira_user_id.lower())
204233
if jira_user_id.lower() == commenter.lower():
205-
logger.debug('skipping incoming JIRA comment as the user id of the comment in JIRA (%s) matches the JIRA username in DefectDojo (%s)', commenter.lower(), jira_user_id.lower())
206-
return HttpResponse('')
234+
return webhook_responser_handler("debug", f"skipping incoming JIRA comment as the user id of the comment in JIRA {commenter.lower()} matches the JIRA username in DefectDojo {jira_user_id.lower()}")
207235

208236
findings = None
209237
if jissue.finding:
210238
findings = [jissue.finding]
211239
create_notification(event='other', title=f'JIRA incoming comment - {jissue.finding}', finding=jissue.finding, url=reverse("view_finding", args=(jissue.finding.id,)), icon='check')
212-
213240
elif jissue.finding_group:
214241
findings = [jissue.finding_group.findings.all()]
215242
create_notification(event='other', title=f'JIRA incoming comment - {jissue.finding}', finding=jissue.finding, url=reverse("view_finding_group", args=(jissue.finding_group.id,)), icon='check')
216-
217243
elif jissue.engagement:
218-
return HttpResponse('Comment for engagement ignored')
244+
return webhook_responser_handler("debug", "Comment for engagement ignored")
219245
else:
220-
raise Http404(f'No finding or engagement found for JIRA issue {jissue.jira_key}')
221-
246+
return webhook_responser_handler("info", f"Received issue update for {jissue.jira_key} for unknown object")
222247
# Set the fields for the notes
223248
author, _ = User.objects.get_or_create(username='JIRA')
224249
entry = f'({commenter_display_name} ({commenter})): {comment_text}'

0 commit comments

Comments
 (0)