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)
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('')