def test_import_with_push_to_jira_add_tags(self):
        import0 = self.import_scan_with_params(self.zap_sample5_filename,
                                               push_to_jira=True)
        test_id = import0['test']
        self.assert_jira_issue_count_in_test(test_id, 2)
        self.assert_jira_group_issue_count_in_test(test_id, 0)

        findings = self.get_test_findings_api(test_id)

        finding = Finding.objects.get(id=findings['results'][0]['id'])

        tags = ['tag1', 'tag2']
        response = self.post_finding_tags_api(finding.id, tags)
        self.patch_finding_api(finding.id, {"push_to_jira": True})

        # Connect to jira to get the new issue
        jira_instance = jira_helper.get_jira_instance(finding)
        jira = jira_helper.get_jira_connection(jira_instance)
        issue = jira.issue(finding.jira_issue.jira_id)

        # Assert that the tags match
        self.assertEqual(issue.fields.labels, tags)

        # by asserting full cassette is played we know all calls to JIRA have been made as expected
        self.assert_cassette_played()
        return test_id
 def get_epic_issues(self, engagement):
     instance = jira_helper.get_jira_instance(engagement)
     jira = jira_helper.get_jira_connection(instance)
     epic_id = jira_helper.get_jira_issue_key(engagement)
     response = {}
     if epic_id:
         url = instance.url.strip('/') + '/rest/agile/1.0/epic/' + epic_id + '/issue'
         response = jira._session.get(url).json()
     return response.get('issues', [])
 def test_get_jira_project_and_instance_no_issue_template(self):
     product = Product.objects.get(id=1)
     jira_project = jira_helper.get_jira_project(product)
     jira_project.issue_template = None
     jira_project.save()
     jira_instance = jira_helper.get_jira_instance(product)
     jira_instance.issue_template = None
     jira_instance.save()
     # no template should return default
     self.assertEqual(jira_helper.get_jira_issue_template(product), 'issue-trackers/jira-description.tpl')
 def assert_jira_issue_in_epic(self, finding, engagement, issue_in_epic=True):
     instance = jira_helper.get_jira_instance(engagement)
     jira = jira_helper.get_jira_connection(instance)
     epic_id = jira_helper.get_jira_issue_key(engagement)
     issue_id = jira_helper.get_jira_issue_key(finding)
     epic_link_field = 'customfield_' + str(get_custom_field(jira, 'Epic Link'))
     url = instance.url.strip('/') + '/rest/api/latest/issue/' + issue_id
     response = jira._session.get(url).json().get('fields', {})
     epic_link = response.get(epic_link_field, None)
     if epic_id is None and epic_link is None or issue_in_epic:
         self.assertTrue(epic_id == epic_link)
     else:
         self.assertTrue(epic_id != epic_link)
    def test_webhook_comment_on_finding_from_dojo_note_with_email(self):
        self.system_settings(enable_jira=True,
                             enable_jira_web_hook=True,
                             disable_jira_webhook_secret=False,
                             jira_webhook_secret=self.correct_secret)

        jira_issue = JIRA_Issue.objects.get(jira_id=2)
        finding = jira_issue.finding
        notes_count_before = finding.notes.count()

        # modify jira_instance to use email instead of name to perform testj
        jira_instance = jira_helper.get_jira_instance(finding)
        jira_instance.username = "******"
        jira_instance.save()

        body = json.loads(
            json.dumps(self.jira_issue_comment_template_json_with_email))
        body['comment']['updateAuthor'][
            'emailAddress'] = "*****@*****.**"
        body['comment']['updateAuthor']['displayName'] = "Defect Dojo"

        response = self.client.post(reverse('jira_web_hook_secret',
                                            args=(self.correct_secret, )),
                                    body,
                                    content_type="application/json")

        jira_issue = JIRA_Issue.objects.get(jira_id=2)
        finding = jira_issue.finding
        notes_count_after = finding.notes.count()

        # reset jira_instance to use name to avoid confusion for potential later tests
        jira_instance = jira_helper.get_jira_instance(finding)
        jira_instance.username = "******"
        jira_instance.save()

        self.assertEqual(200, response.status_code)
        # incoming comment must be ignored
        self.assertEqual(notes_count_after, notes_count_before)
def post_jira_comment(finding, message_factory, heads_up_days=0):
    if not finding or not finding.has_jira_issue:
        return

    jira_project = jira_helper.get_jira_project(finding)

    if jira_project and jira_project.risk_acceptance_expiration_notification:
        jira_instance = jira_helper.get_jira_instance(finding)

        if jira_instance:

            jira_comment = message_factory(None, heads_up_days)

            logger.debug("Creating JIRA comment for something risk acceptance related")
            jira_helper.add_simple_jira_comment(jira_instance, finding.jira_issue, jira_comment)
def jira_status_reconciliation(*args, **kwargs):
    mode = kwargs['mode']
    product = kwargs['product']
    engagement = kwargs['engagement']
    daysback = kwargs['daysback']
    dryrun = kwargs['dryrun']

    logger.debug('mode: %s product:%s engagement: %s dryrun: %s', mode, product, engagement, dryrun)

    if mode and mode not in ('push_status_to_jira', 'import_status_from_jira', 'reconcile'):
        print('mode must be one of reconcile, push_status_to_jira or import_status_from_jira')
        return False

    if not mode:
        mode = 'reconcile'

    findings = Finding.objects.all()
    if product:
        product = Product.objects.filter(name=product).first()
        findings = findings.filter(test__engagement__product=product)

    if engagement:
        engagement = Engagement.objects.filter(name=engagement).first()
        findings = findings.filter(test__engagement=engagement)

    if daysback:
        timestamp = timezone.now() - relativedelta(days=int(daysback))
        findings = findings.filter(created__gte=timestamp)

    findings = findings.exclude(jira_issue__isnull=True)

    # order by product, engagement to increase the cance of being able to reuse jira_instance + jira connection
    findings = findings.order_by('test__engagement__product__id', 'test__engagement__id')

    findings = findings.prefetch_related('jira_issue__jira_project__jira_instance')
    findings = findings.prefetch_related('test__engagement__jira_project__jira_instance')
    findings = findings.prefetch_related('test__engagement__product__jira_project_set__jira_instance')

    logger.debug(findings.query)

    messages = ['jira_key;finding_url;resolution_or_status;find.jira_issue.jira_change;issue_from_jira.fields.updated;find.last_status_update;issue_from_jira.fields.updated;find.last_reviewed;issue_from_jira.fields.updated;flag1;flag2;flag3;action;change_made']
    for find in findings:
        logger.debug('jira status reconciliation for: %i:%s', find.id, find)

        issue_from_jira = jira_helper.get_jira_issue_from_jira(find)

        if not issue_from_jira:
            message = '%s;%s/finding/%d;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;unable to retrieve JIRA Issue;%s' % \
                (find.jira_issue.jira_key, settings.SITE_URL, find.id, find.status(), None, None, None, None,
                            find.jira_issue.jira_change, None, find.last_status_update, None, find.last_reviewed, None, 'error')
            messages.append(message)
            logger.info(message)
            continue

        assignee = issue_from_jira.fields.assignee if hasattr(issue_from_jira.fields, 'assignee') else None
        assignee_name = assignee.displayName if assignee else None
        resolution = issue_from_jira.fields.resolution if issue_from_jira.fields.resolution and issue_from_jira.fields.resolution != "None" else None
        resolution_id = resolution.id if resolution else None
        resolution_name = resolution.name if resolution else None

        # convert from str to datetime
        issue_from_jira.fields.updated = parse_datetime(issue_from_jira.fields.updated)

        find.jira_issue.jira_change, issue_from_jira.fields.updated, find.last_status_update, issue_from_jira.fields.updated, find.last_reviewed, issue_from_jira.fields.updated,

        flag1, flag2, flag3 = None, None, None

        if mode == 'reconcile' and not find.last_status_update:
            message = '%s; %s/finding/%d;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;skipping finding with no last_status_update;%s' % \
                (find.jira_issue.jira_key, settings.SITE_URL, find.id, find.status(), None, None, None, None,
                find.jira_issue.jira_change, issue_from_jira.fields.updated, find.last_status_update, issue_from_jira.fields.updated, find.last_reviewed, issue_from_jira.fields.updated, 'skipped')
            messages.append(message)
            logger.info(message)
            continue
        elif find.risk_accepted:
            message = '%s; %s/finding/%d;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;%sskipping risk accepted findings;%s' % \
                (find.jira_issue.jira_key, settings.SITE_URL, find.id, find.status(), resolution_name, None, None, None,
                find.jira_issue.jira_change, issue_from_jira.fields.updated, find.last_status_update, issue_from_jira.fields.updated, find.last_reviewed, issue_from_jira.fields.updated, 'skipped')
            messages.append(message)
            logger.info(message)
        elif jira_helper.issue_from_jira_is_active(issue_from_jira) and find.active:
            message = '%s; %s/finding/%d;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;no action both sides are active/open;%s' % \
                (find.jira_issue.jira_key, settings.SITE_URL, find.id, find.status(), resolution_name, None, None, None,
                    find.jira_issue.jira_change, issue_from_jira.fields.updated, find.last_status_update, issue_from_jira.fields.updated, find.last_reviewed, issue_from_jira.fields.updated, 'equal')
            messages.append(message)
            logger.info(message)
        elif not jira_helper.issue_from_jira_is_active(issue_from_jira) and not find.active:
            message = '%s; %s/finding/%d;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;no action both sides are inactive/closed;%s' % \
                (find.jira_issue.jira_key, settings.SITE_URL, find.id, find.status(), resolution_name, None, None, None,
                find.jira_issue.jira_change, issue_from_jira.fields.updated, find.last_status_update, issue_from_jira.fields.updated, find.last_reviewed, issue_from_jira.fields.updated, 'equal')
            messages.append(message)
            logger.info(message)

        else:
            # statuses are different
            if mode in ('push_status_to_jira', 'import_status_from_jira'):
                action = mode
            else:
                # reconcile
                # Status is JIRA is newer if:
                # dojo.jira_change < jira.updated, and
                # dojo.last_status_update < jira.updated, and
                # dojo.last_reviewed < jira.update,

                flag1 = (not find.jira_issue.jira_change or (find.jira_issue.jira_change < issue_from_jira.fields.updated))
                flag2 = not find.last_status_update or (find.last_status_update < issue_from_jira.fields.updated)
                flag3 = (not find.last_reviewed or (find.last_reviewed < issue_from_jira.fields.updated))

                logger.debug('%s,%s,%s,%s', resolution_name, flag1, flag2, flag3)

                if flag1 and flag2 and flag3:
                    action = 'import_status_from_jira'

                else:
                    # Status is DOJO is newer if:
                    # dojo.jira_change > jira.updated or # can't happen
                    # dojo.last_status_update > jira.updated or
                    # dojo.last_reviewed > jira.updated
                    # dojo.mitigated > dojo.jira_change

                    flag1 = not find.jira_issue.jira_change or (find.jira_issue.jira_change > issue_from_jira.fields.updated)
                    flag2 = find.last_status_update > issue_from_jira.fields.updated
                    flag3 = find.is_Mitigated and find.mitigated and find.jira_issue.jira_change and find.mitigated > find.jira_issue.jira_change

                    logger.debug('%s,%s,%s,%s', resolution_name, flag1, flag2, flag3)

                    if flag1 or flag2 or flag3:
                        action = 'push_status_to_jira'

            prev_jira_instance, jira = None, None

            if action == 'import_status_from_jira':
                message_action = 'deactivating' if find.active else 'reactivating'

                status_changed = jira_helper.process_resolution_from_jira(find, resolution_id, resolution_name, assignee_name, issue_from_jira.fields.updated) if not dryrun else 'dryrun'
                if status_changed:
                    message = '%s; %s/finding/%d;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s finding in defectdojo;%s' % \
                        (find.jira_issue.jira_key, settings.SITE_URL, find.id, find.status(), resolution_name, flag1, flag2, flag3,
                        find.jira_issue.jira_change, issue_from_jira.fields.updated, find.last_status_update, issue_from_jira.fields.updated, find.last_reviewed, issue_from_jira.fields.updated, message_action, status_changed)
                    messages.append(message)
                    logger.info(message)
                else:
                    message = '%s; %s/finding/%d;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;no changes made from jira resolution;%s' % \
                        (find.jira_issue.jira_key, settings.SITE_URL, find.id, find.status(), resolution_name, flag1, flag2, flag3,
                        find.jira_issue.jira_change, issue_from_jira.fields.updated, find.last_status_update, issue_from_jira.fields.updated, find.last_reviewed, issue_from_jira.fields.updated, status_changed)
                    messages.append(message)
                    logger.info(message)

            elif action == 'push_status_to_jira':
                jira_instance = jira_helper.get_jira_instance(find)
                if not prev_jira_instance or (jira_instance.id != prev_jira_instance.id):
                    # only reconnect to jira if the instance if different from the previous finding
                    jira = jira_helper.get_jira_connection(jira_instance)

                message_action = 'reopening' if find.active else 'closing'

                status_changed = jira_helper.push_status_to_jira(find, jira_instance, jira, issue_from_jira, save=True) if not dryrun else 'dryrun'

                if status_changed:
                    message = '%s; %s/finding/%d;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s jira issue;%s;' % \
                        (find.jira_issue.jira_key, settings.SITE_URL, find.id, find.status(), resolution_name, flag1, flag2, flag3, message_action,
                        find.jira_issue.jira_change, issue_from_jira.fields.updated, find.last_status_update, issue_from_jira.fields.updated, find.last_reviewed, issue_from_jira.fields.updated, status_changed)
                    messages.append(message)
                    logger.info(message)
                else:
                    if status_changed is None:
                        status_changed = 'Error'
                    message = '%s; %s/finding/%d;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;no changes made while pushing status to jira;%s' % \
                        (find.jira_issue.jira_key, settings.SITE_URL, find.id, find.status(), resolution_name, flag1, flag2, flag3,
                        find.jira_issue.jira_change, issue_from_jira.fields.updated, find.last_status_update, issue_from_jira.fields.updated, find.last_reviewed, issue_from_jira.fields.updated, status_changed)
                    messages.append(message)

                    logger.info(message)
            else:
                message = '%s; %s/finding/%d;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;unable to determine source of truth;%s' % \
                    (find.jira_issue.jira_key, settings.SITE_URL, find.id, find.status(), resolution_name, flag1, flag2, flag3,
                    find.jira_issue.jira_change, issue_from_jira.fields.updated, find.last_status_update, issue_from_jira.fields.updated, find.last_reviewed, issue_from_jira.fields.updated, status_changed)
                messages.append(message)

                logger.info(message)

    logger.info('results (semicolon seperated)')
    for message in messages:
        print(message)
Beispiel #8
0
def webhook(request, secret=None):
    if not get_system_setting('enable_jira'):
        logger.debug('ignoring incoming webhook as JIRA is disabled.')
        raise Http404('JIRA disabled')
    elif not get_system_setting('enable_jira_web_hook'):
        logger.debug('ignoring incoming webhook as JIRA Webhook is disabled.')
        raise Http404('JIRA Webhook disabled')
    elif not get_system_setting('disable_jira_webhook_secret'):
        if not get_system_setting('jira_webhook_secret'):
            logger.warning(
                'ignoring incoming webhook as JIRA Webhook secret is empty in Defect Dojo system settings.'
            )
            raise PermissionDenied('JIRA Webhook secret cannot be empty')
        if secret != get_system_setting('jira_webhook_secret'):
            logger.warning('invalid secret provided to JIRA Webhook')
            raise PermissionDenied(
                'invalid or no secret provided to JIRA Webhook')

    # if webhook secret is disabled in system_settings, we ignore the incoming secret, even if it doesn't match

    # example json bodies at the end of this file

    if request.content_type != 'application/json':
        return HttpResponseBadRequest("only application/json supported")

    if request.method == 'POST':
        try:
            parsed = json.loads(request.body.decode('utf-8'))
            if parsed.get('webhookEvent') == 'jira:issue_updated':
                # xml examples at the end of file
                jid = parsed['issue']['id']
                jissue = get_object_or_404(JIRA_Issue, jira_id=jid)
                logging.info("Received issue update for {}".format(
                    jissue.jira_key))
                if jissue.finding:
                    finding = jissue.finding
                    jira_instance = jira_helper.get_jira_instance(finding)
                    resolved = True
                    resolution = parsed['issue']['fields']['resolution']

                    #         "resolution":{
                    #             "self":"http://www.testjira.com/rest/api/2/resolution/11",
                    #             "id":"11",
                    #             "description":"Cancelled by the customer.",
                    #             "name":"Cancelled"
                    #         },

                    # or
                    #         "resolution": null

                    if resolution is None:
                        resolved = False
                        logger.debug(
                            "JIRA resolution is None, therefore resolved is now False"
                        )
                    if finding.active is resolved:
                        if finding.active:
                            if jira_instance and resolution[
                                    'name'] in jira_instance.accepted_resolutions:
                                logger.debug(
                                    "Marking related finding of {} as accepted. Creating risk acceptance."
                                    .format(jissue.jira_key))
                                finding.active = False
                                finding.mitigated = None
                                finding.is_Mitigated = False
                                finding.false_p = False
                                assignee = parsed['issue']['fields'].get(
                                    'assignee')
                                assignee_name = assignee[
                                    'name'] if assignee else None
                                Risk_Acceptance.objects.create(
                                    accepted_by=assignee_name,
                                    owner=finding.reporter,
                                ).accepted_findings.set([finding])
                            elif jira_instance and resolution[
                                    'name'] in jira_instance.false_positive_resolutions:
                                logger.debug(
                                    "Marking related finding of {} as false-positive"
                                    .format(jissue.jira_key))
                                finding.active = False
                                finding.verified = False
                                finding.mitigated = None
                                finding.is_Mitigated = False
                                finding.false_p = True
                                ra_helper.remove_from_any_risk_acceptance(
                                    finding)
                            else:
                                # Mitigated by default as before
                                logger.debug(
                                    "Marking related finding of {} as mitigated (default)"
                                    .format(jissue.jira_key))
                                now = timezone.now()
                                finding.active = False
                                finding.mitigated = now
                                finding.is_Mitigated = True
                                finding.endpoints.clear()
                                finding.false_p = False
                                ra_helper.remove_from_any_risk_acceptance(
                                    finding)
                        else:
                            # Reopen / Open Jira issue
                            logger.debug(
                                "Re-opening related finding of {}".format(
                                    jissue.jira_key))
                            finding.active = True
                            finding.mitigated = None
                            finding.is_Mitigated = False
                            finding.false_p = False
                            ra_helper.remove_from_any_risk_acceptance(finding)
                    else:
                        # Reopen / Open Jira issue
                        finding.active = True
                        finding.mitigated = None
                        finding.is_Mitigated = False
                        finding.false_p = False
                        ra_helper.remove_from_any_risk_acceptance(finding)

                    finding.jira_issue.jira_change = timezone.now()
                    finding.jira_issue.save()
                    finding.save()

                elif jissue.engagement:
                    # if parsed['issue']['fields']['resolution'] != None:
                    #     eng.active = False
                    #     eng.status = 'Completed'
                    #     eng.save()
                    return HttpResponse('Update for engagement ignored')
                else:
                    raise Http404(
                        'No finding or engagement found for JIRA issue {}'.
                        format(jissue.jira_key))

            if parsed.get('webhookEvent') == 'comment_created':
                """
                    example incoming requests from JIRA Server 8.14.0
                    {
                    "timestamp":1610269967824,
                    "webhookEvent":"comment_created",
                    "comment":{
                        "self":"https://jira.host.com/rest/api/2/issue/115254/comment/466578",
                        "id":"466578",
                        "author":{
                            "self":"https://jira.host.com/rest/api/2/user?username=defect.dojo",
                            "name":"defect.dojo",
                            "key":"defect.dojo", # seems to be only present on JIRA Server, not on Cloud
                            "avatarUrls":{
                                "48x48":"https://www.gravatar.com/avatar/9637bfb970eff6176357df615f548f1c?d=mm&s=48",
                                "24x24":"https://www.gravatar.com/avatar/9637bfb970eff6176357df615f548f1c?d=mm&s=24",
                                "16x16":"https://www.gravatar.com/avatar9637bfb970eff6176357df615f548f1c?d=mm&s=16",
                                "32x32":"https://www.gravatar.com/avatar/9637bfb970eff6176357df615f548f1c?d=mm&s=32"
                            },
                            "displayName":"Defect Dojo",
                            "active":true,
                            "timeZone":"Europe/Amsterdam"
                        },
                        "body":"(Valentijn Scholten):test4",
                        "updateAuthor":{
                            "self":"https://jira.host.com/rest/api/2/user?username=defect.dojo",
                            "name":"defect.dojo",
                            "key":"defect.dojo",
                            "avatarUrls":{
                                "48x48":"https://www.gravatar.com/avatar/9637bfb970eff6176357df615f548f1c?d=mm&s=48",
                                "24x24""https://www.gravatar.com/avatar/9637bfb970eff6176357df615f548f1c?d=mm&s=24",
                                "16x16":"https://www.gravatar.com/avatar/9637bfb970eff6176357df615f548f1c?d=mm&s=16",
                                "32x32":"https://www.gravatar.com/avatar/9637bfb970eff6176357df615f548f1c?d=mm&s=32"
                            },
                            "displayName":"Defect Dojo",
                            "active":true,
                            "timeZone":"Europe/Amsterdam"
                        },
                        "created":"2021-01-10T10:12:47.824+0100",
                        "updated":"2021-01-10T10:12:47.824+0100"
                    }
                    }
                """

                comment_text = parsed['comment']['body']
                commentor = ''
                if 'name' in parsed['comment']['updateAuthor']:
                    commentor = parsed['comment']['updateAuthor']['name']
                elif 'emailAddress' in parsed['comment']['updateAuthor']:
                    commentor = parsed['comment']['updateAuthor'][
                        'emailAddress']
                else:
                    logger.debug(
                        'Could not find the author of this jira comment!')
                commentor_display_name = parsed['comment']['updateAuthor'][
                    'displayName']
                # example: body['comment']['self'] = "http://www.testjira.com/jira_under_a_path/rest/api/2/issue/666/comment/456843"
                jid = parsed['comment']['self'].split('/')[-3]
                jissue = get_object_or_404(JIRA_Issue, jira_id=jid)
                logging.info("Received issue comment for {}".format(
                    jissue.jira_key))
                logger.debug('jissue: %s', vars(jissue))
                if jissue.finding:
                    # logger.debug('finding: %s', vars(jissue.finding))
                    jira_usernames = JIRA_Instance.objects.values_list(
                        'username', flat=True)
                    for jira_userid in jira_usernames:
                        # logger.debug('incoming username: %s jira config username: %s', commentor.lower(), jira_userid.lower())
                        if jira_userid.lower() == commentor.lower():
                            logger.debug(
                                'skipping incoming JIRA comment as the user id of the comment in JIRA (%s) matches the JIRA username in DefectDojo (%s)',
                                commentor.lower(), jira_userid.lower())
                            return HttpResponse('')
                            break
                    finding = jissue.finding
                    new_note = Notes()
                    new_note.entry = '(%s (%s)): %s' % (
                        commentor_display_name, commentor, comment_text)
                    new_note.author, created = User.objects.get_or_create(
                        username='******')
                    new_note.save()
                    finding.notes.add(new_note)
                    finding.jira_issue.jira_change = timezone.now()
                    finding.jira_issue.save()
                    finding.save()
                    create_notification(
                        event='other',
                        title='JIRA incoming comment - %s' % (jissue.finding),
                        url=reverse("view_finding",
                                    args=(jissue.finding.id, )),
                        icon='check')
                elif jissue.engagement:
                    return HttpResponse('Comment for engagement ignored')
                else:
                    raise Http404(
                        'No finding or engagement found for JIRA issue {}'.
                        format(jissue.jira_key))

            if parsed.get('webhookEvent') not in [
                    'comment_created', 'jira:issue_updated'
            ]:
                logger.info(
                    'Unrecognized JIRA webhook event received: {}'.format(
                        parsed.get('webhookEvent')))
        except Exception as e:
            if isinstance(e, Http404):
                logger.warning('404 error processing JIRA webhook')
            else:
                logger.exception(e)

            try:
                logger.debug('jira_webhook_body_parsed:')
                logger.debug(json.dumps(parsed, indent=4))
            except:
                logger.debug('jira_webhook_body:')
                logger.debug(request.body.decode('utf-8'))

            # reraise to make sure we don't silently swallow things
            raise
    return HttpResponse('')