-
Notifications
You must be signed in to change notification settings - Fork 1.6k
/
helper.py
1707 lines (1389 loc) · 63.1 KB
/
helper.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import io
import json
import logging
import os
from pathlib import Path
from typing import Any
import requests
from django.conf import settings
from django.contrib import messages
from django.template import TemplateDoesNotExist
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import timezone
from jira import JIRA
from jira.exceptions import JIRAError
from requests.auth import HTTPBasicAuth
from dojo.celery import app
from dojo.decorators import dojo_async_task, dojo_model_from_id, dojo_model_to_id
from dojo.forms import JIRAEngagementForm, JIRAProjectForm
from dojo.models import (
Engagement,
Finding,
Finding_Group,
JIRA_Instance,
JIRA_Issue,
JIRA_Project,
Notes,
Product,
Risk_Acceptance,
Stub_Finding,
System_Settings,
Test,
User,
)
from dojo.notifications.helper import create_notification
from dojo.utils import (
add_error_message_to_response,
get_file_images,
get_system_setting,
prod_name,
to_str_typed,
truncate_with_dots,
)
logger = logging.getLogger(__name__)
RESOLVED_STATUS = [
"Inactive",
"Mitigated",
"False Positive",
"Out of Scope",
"Duplicate",
]
OPEN_STATUS = [
"Active",
"Verified",
]
def is_jira_enabled():
if not get_system_setting("enable_jira"):
logger.debug("JIRA is disabled, not doing anything")
return False
return True
def is_jira_configured_and_enabled(obj):
if not is_jira_enabled():
return False
jira_project = get_jira_project(obj)
if jira_project is None:
logger.debug('JIRA project not found for: "%s" not doing anything', obj)
return False
return jira_project.enabled
def is_push_to_jira(instance, push_to_jira_parameter=None):
if not is_jira_configured_and_enabled(instance):
return False
jira_project = get_jira_project(instance)
# caller explicitly stated true or false (False is different from None!)
if push_to_jira_parameter is not None:
return push_to_jira_parameter
# Check to see if jira project is disabled to prevent pushing findings
if not jira_project.enabled:
return False
# push_to_jira was not specified, so look at push_all_issues in JIRA_Project
return jira_project.push_all_issues
def is_push_all_issues(instance):
if not is_jira_configured_and_enabled(instance):
return False
if jira_project := get_jira_project(instance):
# Check to see if jira project is disabled to prevent pushing findings
if not jira_project.enabled:
return None
return jira_project.push_all_issues
return None
# checks if a finding can be pushed to JIRA
# optionally provides a form with the new data for the finding
# any finding that already has a JIRA issue can be pushed again to JIRA
# returns True/False, error_message, error_code
def can_be_pushed_to_jira(obj, form=None):
# logger.debug('can be pushed to JIRA: %s', finding_or_form)
jira_project = get_jira_project(obj)
if not jira_project:
return False, f"{to_str_typed(obj)} cannot be pushed to jira as there is no jira project configuration for this product.", "error_no_jira_project"
if not jira_project.enabled:
return False, f"{to_str_typed(obj)} cannot be pushed to jira as the jira project is not enabled.", "error_no_jira_project"
if not hasattr(obj, "has_jira_issue"):
return False, f"{to_str_typed(obj)} cannot be pushed to jira as there is no jira_issue attribute.", "error_no_jira_issue_attribute"
if isinstance(obj, Stub_Finding):
# stub findings don't have active/verified/etc and can always be pushed
return True, None, None
if obj.has_jira_issue:
# findings or groups already having an existing jira issue can always be pushed
return True, None, None
if isinstance(obj, Finding):
if form:
active = form["active"].value()
verified = form["verified"].value()
severity = form["severity"].value()
else:
active = obj.active
verified = obj.verified
severity = obj.severity
logger.debug("can_be_pushed_to_jira: %s, %s, %s", active, verified, severity)
isenforced = get_system_setting("enforce_verified_status", True)
if not active or (not verified and isenforced):
logger.debug("Findings must be active and verified, if enforced by system settings, to be pushed to JIRA")
return False, "Findings must be active and verified, if enforced by system settings, to be pushed to JIRA", "not_active_or_verified"
jira_minimum_threshold = None
if System_Settings.objects.get().jira_minimum_severity:
jira_minimum_threshold = Finding.get_number_severity(System_Settings.objects.get().jira_minimum_severity)
if jira_minimum_threshold and jira_minimum_threshold > Finding.get_number_severity(severity):
logger.debug(f"Finding below the minimum JIRA severity threshold ({System_Settings.objects.get().jira_minimum_severity}).")
return False, f"Finding below the minimum JIRA severity threshold ({System_Settings.objects.get().jira_minimum_severity}).", "below_minimum_threshold"
elif isinstance(obj, Finding_Group):
if not obj.findings.all():
return False, f"{to_str_typed(obj)} cannot be pushed to jira as it is empty.", "error_empty"
# Accommodating a strange behavior where a finding group sometimes prefers `obj.status` rather than `obj.status()`
try:
not_active = "Active" not in obj.status()
except TypeError: # TypeError: 'str' object is not callable
not_active = "Active" not in obj.status
# Determine if the finding group is not active
if not_active:
return False, f"{to_str_typed(obj)} cannot be pushed to jira as it is not active.", "error_inactive"
else:
return False, f"{to_str_typed(obj)} cannot be pushed to jira as it is of unsupported type.", "error_unsupported"
return True, None, None
# use_inheritance=True means get jira_project config from product if engagement itself has none
def get_jira_project(obj, use_inheritance=True):
if not is_jira_enabled():
return None
if obj is None:
return None
# logger.debug('get jira project for: ' + str(obj.id) + ':' + str(obj))
if isinstance(obj, JIRA_Project):
return obj
if isinstance(obj, JIRA_Issue):
if obj.jira_project:
return obj.jira_project
# some old jira_issue records don't have a jira_project, so try to go via the finding instead
if hasattr(obj, "finding") and obj.finding:
return get_jira_project(obj.finding, use_inheritance=use_inheritance)
if hasattr(obj, "engagement") and obj.engagement:
return get_jira_project(obj.finding, use_inheritance=use_inheritance)
return None
if isinstance(obj, Finding) or isinstance(obj, Stub_Finding):
finding = obj
return get_jira_project(finding.test)
if isinstance(obj, Finding_Group):
return get_jira_project(obj.test)
if isinstance(obj, Test):
test = obj
return get_jira_project(test.engagement)
if isinstance(obj, Engagement):
engagement = obj
jira_project = None
try:
jira_project = engagement.jira_project # first() doesn't work with prefetching
if jira_project:
logger.debug("found jira_project %s for %s", jira_project, engagement)
return jira_project
except JIRA_Project.DoesNotExist:
pass # leave jira_project as None
if use_inheritance:
logger.debug("delegating to product %s for %s", engagement.product, engagement)
return get_jira_project(engagement.product)
logger.debug("not delegating to product %s for %s", engagement.product, engagement)
return None
if isinstance(obj, Product):
# TODO: refactor relationships, but now this would brake APIv1 (and v2?)
product = obj
jira_projects = product.jira_project_set.all() # first() doesn't work with prefetching
jira_project = jira_projects[0] if len(jira_projects) > 0 else None
if jira_project:
logger.debug("found jira_project %s for %s", jira_project, product)
return jira_project
logger.debug("no jira_project found for %s", obj)
return None
def get_jira_instance(obj):
if not is_jira_enabled():
return None
jira_project = get_jira_project(obj)
if jira_project:
logger.debug("found jira_instance %s for %s", jira_project.jira_instance, obj)
return jira_project.jira_instance
return None
def get_jira_url(obj):
logger.debug("getting jira url")
# finding + engagement
issue = get_jira_issue(obj)
if issue is not None:
return get_jira_issue_url(issue)
if isinstance(obj, Finding):
# finding must only have url if there is a jira_issue
# engagement can continue to show url of jiraproject instead of jira issue
return None
if isinstance(obj, JIRA_Project):
return get_jira_project_url(obj)
return get_jira_project_url(get_jira_project(obj))
def get_jira_issue_url(issue):
logger.debug("getting jira issue url")
jira_project = get_jira_project(issue)
jira_instance = get_jira_instance(jira_project)
if jira_instance is None:
return None
# example http://jira.com/browser/SEC-123
return jira_instance.url + "/browse/" + issue.jira_key
def get_jira_project_url(obj):
logger.debug("getting jira project url")
if not isinstance(obj, JIRA_Project):
jira_project = get_jira_project(obj)
else:
jira_project = obj
if jira_project:
logger.debug("getting jira project url2")
jira_instance = get_jira_instance(obj)
if jira_project and jira_instance:
logger.debug("getting jira project url3")
return jira_project.jira_instance.url + "/browse/" + jira_project.project_key
return None
def get_jira_key(obj):
if hasattr(obj, "has_jira_issue") and obj.has_jira_issue:
return get_jira_issue_key(obj)
if isinstance(obj, JIRA_Project):
return get_jira_project_key(obj)
return get_jira_project_key(get_jira_project(obj))
def get_jira_issue_key(obj):
if obj.has_jira_issue:
return obj.jira_issue.jira_key
return None
def get_jira_project_key(obj):
jira_project = get_jira_project(obj)
if not get_jira_project:
return None
return jira_project.project_key
def get_jira_issue_template(obj):
jira_project = get_jira_project(obj)
template_dir = jira_project.issue_template_dir
if not template_dir:
jira_instance = get_jira_instance(obj)
template_dir = jira_instance.issue_template_dir
# fallback to default as before
if not template_dir:
template_dir = "issue-trackers/jira_full/"
if isinstance(obj, Finding_Group):
return os.path.join(template_dir, "jira-finding-group-description.tpl")
return os.path.join(template_dir, "jira-description.tpl")
def get_jira_creation(obj):
if isinstance(obj, Finding) or isinstance(obj, Engagement) or isinstance(obj, Finding_Group):
if obj.has_jira_issue:
return obj.jira_issue.jira_creation
return None
def get_jira_change(obj):
if isinstance(obj, Finding) or isinstance(obj, Engagement) or isinstance(obj, Finding_Group):
if obj.has_jira_issue:
return obj.jira_issue.jira_change
else:
logger.debug("get_jira_change unsupported object type: %s", obj)
return None
def get_epic_name_field_name(jira_instance):
if not jira_instance or not jira_instance.epic_name_id:
return None
return "customfield_" + str(jira_instance.epic_name_id)
def has_jira_issue(obj):
return get_jira_issue(obj) is not None
def get_jira_issue(obj):
if isinstance(obj, Finding) or isinstance(obj, Engagement) or isinstance(obj, Finding_Group):
try:
return obj.jira_issue
except JIRA_Issue.DoesNotExist:
return None
return None
def has_jira_configured(obj):
return get_jira_project(obj) is not None
def connect_to_jira(jira_server, jira_username, jira_password):
return JIRA(
server=jira_server,
basic_auth=(jira_username, jira_password),
max_retries=0,
options={
"verify": settings.JIRA_SSL_VERIFY,
"headers": settings.ADDITIONAL_HEADERS,
})
def get_jira_connect_method():
if hasattr(settings, "JIRA_CONNECT_METHOD"):
try:
import importlib
mn, _, fn = settings.JIRA_CONNECT_METHOD.rpartition(".")
m = importlib.import_module(mn)
return getattr(m, fn)
except ModuleNotFoundError:
pass
return connect_to_jira
def get_jira_connection_raw(jira_server, jira_username, jira_password):
try:
connect_method = get_jira_connect_method()
jira = connect_method(jira_server, jira_username, jira_password)
logger.debug("logged in to JIRA %s successfully", jira_server)
return jira
except JIRAError as e:
logger.exception(e)
error_message = e.text if hasattr(e, "text") else e.message if hasattr(e, "message") else e.args[0]
if e.status_code in [401, 403]:
log_jira_generic_alert("JIRA Authentication Error", error_message)
else:
log_jira_generic_alert("Unknown JIRA Connection Error", error_message)
add_error_message_to_response("Unable to authenticate to JIRA. Please check the URL, username, password, captcha challenge, Network connection. Details in alert on top right. " + str(error_message))
raise
except requests.exceptions.RequestException as re:
logger.exception(re)
error_message = re.text if hasattr(re, "text") else re.message if hasattr(re, "message") else re.args[0]
log_jira_generic_alert("Unknown JIRA Connection Error", re)
add_error_message_to_response("Unable to authenticate to JIRA. Please check the URL, username, password, captcha challenge, Network connection. Details in alert on top right. " + str(error_message))
raise
# Gets a connection to a Jira server based on the finding
def get_jira_connection(obj):
jira_instance = obj
if not isinstance(jira_instance, JIRA_Instance):
jira_instance = get_jira_instance(obj)
if jira_instance is not None:
return get_jira_connection_raw(jira_instance.url, jira_instance.username, jira_instance.password)
return None
def jira_get_resolution_id(jira, issue, status):
transitions = jira.transitions(issue)
resolution_id = None
for t in transitions:
if t["name"] == "Resolve Issue":
resolution_id = t["id"]
break
if t["name"] == "Reopen Issue":
resolution_id = t["id"]
break
return resolution_id
def jira_transition(jira, issue, transition_id):
try:
if issue and transition_id:
jira.transition_issue(issue, transition_id)
return True
except JIRAError as jira_error:
logger.debug("error transitioning jira issue " + issue.key + " " + str(jira_error))
logger.exception(jira_error)
alert_text = f"JiraError HTTP {jira_error.status_code}"
if jira_error.url:
alert_text += f" url: {jira_error.url}"
if jira_error.text:
alert_text += f"\ntext: {jira_error.text}"
log_jira_generic_alert("error transitioning jira issue " + issue.key, alert_text)
return None
# Used for unit testing so geting all the connections is manadatory
def get_jira_updated(finding):
if finding.has_jira_issue:
j_issue = finding.jira_issue.jira_id
elif finding.finding_group and finding.finding_group.has_jira_issue:
j_issue = finding.finding_group.jira_issue.jira_id
if j_issue:
project = get_jira_project(finding)
issue = jira_get_issue(project, j_issue)
return issue.fields.updated
return None
# Used for unit testing so geting all the connections is manadatory
def get_jira_status(finding):
if finding.has_jira_issue:
j_issue = finding.jira_issue.jira_id
elif finding.finding_group and finding.finding_group.has_jira_issue:
j_issue = finding.finding_group.jira_issue.jira_id
if j_issue:
project = get_jira_project(finding)
issue = jira_get_issue(project, j_issue)
return issue.fields.status
return None
# Used for unit testing so geting all the connections is manadatory
def get_jira_comments(finding):
if finding.has_jira_issue:
j_issue = finding.jira_issue.jira_id
elif finding.finding_group and finding.finding_group.has_jira_issue:
j_issue = finding.finding_group.jira_issue.jira_id
if j_issue:
project = get_jira_project(finding)
issue = jira_get_issue(project, j_issue)
return issue.fields.comment.comments
return None
# Logs the error to the alerts table, which appears in the notification toolbar
def log_jira_generic_alert(title, description):
create_notification(
event="jira_update",
title=title,
description=description,
icon="bullseye",
source="JIRA")
# Logs the error to the alerts table, which appears in the notification toolbar
def log_jira_alert(error, obj):
create_notification(
event="jira_update",
title="Error pushing to JIRA " + "(" + truncate_with_dots(prod_name(obj), 25) + ")",
description=to_str_typed(obj) + ", " + error,
url=obj.get_absolute_url(),
icon="bullseye",
source="Push to JIRA",
obj=obj)
# Displays an alert for Jira notifications
def log_jira_message(text, finding):
create_notification(
event="jira_update",
title="Pushing to JIRA: ",
description=text + " Finding: " + str(finding.id),
url=reverse("view_finding", args=(finding.id, )),
icon="bullseye",
source="JIRA", finding=finding)
def get_labels(obj):
# Update Label with system settings label
labels = []
system_settings = System_Settings.objects.get()
system_labels = system_settings.jira_labels
prod_name_label = prod_name(obj).replace(" ", "_")
jira_project = get_jira_project(obj)
if system_labels:
system_labels = system_labels.split()
for system_label in system_labels:
labels.append(system_label)
# Update the label with the product name (underscore)
labels.append(prod_name_label)
# labels per-product/engagement
if jira_project and jira_project.jira_labels:
project_labels = jira_project.jira_labels.split()
for project_label in project_labels:
labels.append(project_label)
# Update the label with the product name (underscore)
if prod_name_label not in labels:
labels.append(prod_name_label)
if system_settings.add_vulnerability_id_to_jira_label or (jira_project and jira_project.add_vulnerability_id_to_jira_label):
if isinstance(obj, Finding) and obj.vulnerability_ids:
for id in obj.vulnerability_ids:
labels.append(id)
elif isinstance(obj, Finding_Group):
for finding in obj.findings.all():
for id in finding.vulnerability_ids:
labels.append(id)
return labels
def get_tags(obj):
# Update Label with system setttings label
tags = []
if isinstance(obj, Finding) or isinstance(obj, Engagement):
obj_tags = obj.tags.all()
if obj_tags:
for tag in obj_tags:
tags.append(str(tag.name.replace(" ", "-")))
if isinstance(obj, Finding_Group):
for finding in obj.findings.all():
obj_tags = finding.tags.all()
if obj_tags:
for tag in obj_tags:
if tag not in tags:
tags.append(str(tag.name.replace(" ", "-")))
return tags
def jira_summary(obj):
summary = ""
if isinstance(obj, Finding):
summary = obj.title
if isinstance(obj, Finding_Group):
summary = obj.name
return summary.replace("\r", "").replace("\n", "")[:255]
def jira_description(obj):
template = get_jira_issue_template(obj)
logger.debug("rendering description for jira from: %s", template)
kwargs = {}
if isinstance(obj, Finding):
kwargs["finding"] = obj
elif isinstance(obj, Finding_Group):
kwargs["finding_group"] = obj
description = render_to_string(template, kwargs)
logger.debug("rendered description: %s", description)
return description
def jira_priority(obj):
return get_jira_instance(obj).get_priority(obj.severity)
def jira_environment(obj):
if isinstance(obj, Finding):
return "\n".join([str(endpoint) for endpoint in obj.endpoints.all()])
if isinstance(obj, Finding_Group):
envs = [
jira_environment(finding)
for finding in obj.findings.all()
]
jira_environments = [env for env in envs if env]
return "\n".join(jira_environments)
return ""
def push_to_jira(obj, *args, **kwargs):
if obj is None:
msg = "Cannot push None to JIRA"
raise ValueError(msg)
if isinstance(obj, Finding):
finding = obj
if finding.has_jira_issue:
return update_jira_issue_for_finding(finding, *args, **kwargs)
return add_jira_issue_for_finding(finding, *args, **kwargs)
if isinstance(obj, Engagement):
engagement = obj
if engagement.has_jira_issue:
return update_epic(engagement, *args, **kwargs)
return add_epic(engagement, *args, **kwargs)
if isinstance(obj, Finding_Group):
group = obj
if group.has_jira_issue:
return update_jira_issue_for_finding_group(group, *args, **kwargs)
return add_jira_issue_for_finding_group(group, *args, **kwargs)
logger.error("unsupported object passed to push_to_jira: %s %i %s", obj.__name__, obj.id, obj)
return None
def add_issues_to_epic(jira, obj, epic_id, issue_keys, ignore_epics=True):
try:
return jira.add_issues_to_epic(epic_id=epic_id, issue_keys=issue_keys, ignore_epics=ignore_epics)
except JIRAError as e:
logger.error("error adding issues %s to epic %s for %s", issue_keys, epic_id, obj.id)
logger.exception(e)
log_jira_alert(e.text, obj)
return False
# we need two separate celery tasks due to the decorators we're using to map to/from ids
@dojo_model_to_id
@dojo_async_task
@app.task
@dojo_model_from_id
def add_jira_issue_for_finding(finding, *args, **kwargs):
return add_jira_issue(finding, *args, **kwargs)
@dojo_model_to_id
@dojo_async_task
@app.task
@dojo_model_from_id(model=Finding_Group)
def add_jira_issue_for_finding_group(finding_group, *args, **kwargs):
return add_jira_issue(finding_group, *args, **kwargs)
def prepare_jira_issue_fields(
project_key,
issuetype_name,
summary,
description,
component_name=None,
custom_fields=None,
labels=None,
environment=None,
priority_name=None,
epic_name_field=None,
default_assignee=None,
duedate=None,
issuetype_fields=[]):
fields = {
"project": {"key": project_key},
"issuetype": {"name": issuetype_name},
"summary": summary,
"description": description,
}
if component_name:
fields["components"] = [{"name": component_name}]
if custom_fields:
fields.update(custom_fields)
if labels and "labels" in issuetype_fields:
fields["labels"] = labels
if environment and "environment" in issuetype_fields:
fields["environment"] = environment
if priority_name and "priority" in issuetype_fields:
fields["priority"] = {"name": priority_name}
if epic_name_field and epic_name_field in issuetype_fields:
fields[epic_name_field] = summary
if duedate and "duedate" in issuetype_fields:
fields["duedate"] = duedate.strftime("%Y-%m-%d")
if default_assignee:
fields["assignee"] = {"name": default_assignee}
return fields
def add_jira_issue(obj, *args, **kwargs):
def failure_to_add_message(message: str, exception: Exception, object: Any) -> bool:
if exception:
logger.exception(exception)
logger.error(message)
log_jira_alert(message, obj)
return False
logger.info("trying to create a new jira issue for %d:%s", obj.id, to_str_typed(obj))
if not is_jira_enabled():
return False
if not is_jira_configured_and_enabled(obj):
message = f"Object {obj.id} cannot be pushed to JIRA as there is no JIRA configuration for {to_str_typed(obj)}."
return failure_to_add_message(message, None, obj)
jira_project = get_jira_project(obj)
jira_instance = get_jira_instance(obj)
obj_can_be_pushed_to_jira, error_message, _error_code = can_be_pushed_to_jira(obj)
if not obj_can_be_pushed_to_jira:
if isinstance(obj, Finding) and obj.duplicate and not obj.active:
logger.warning("%s will not be pushed to JIRA as it's a duplicate finding", to_str_typed(obj))
else:
log_jira_alert(error_message, obj)
logger.warning("%s cannot be pushed to JIRA: %s.", to_str_typed(obj), error_message)
logger.warning("The JIRA issue will NOT be created.")
return False
logger.debug("Trying to create a new JIRA issue for %s...", to_str_typed(obj))
# Attempt to get the jira connection
try:
JIRAError.log_to_tempfile = False
jira = get_jira_connection(jira_instance)
except Exception as e:
message = f"The following jira instance could not be connected: {jira_instance} - {e.text}"
return failure_to_add_message(message, e, obj)
# Set the list of labels to set on the jira issue
labels = get_labels(obj) + get_tags(obj)
if labels:
labels = list(dict.fromkeys(labels)) # de-dup
# Determine what due date to set on the jira issue
duedate = None
if System_Settings.objects.get().enable_finding_sla:
duedate = obj.sla_deadline()
# Set the fields that will compose the jira issue
try:
issuetype_fields = get_issuetype_fields(jira, jira_project.project_key, jira_instance.default_issue_type)
fields = prepare_jira_issue_fields(
project_key=jira_project.project_key,
issuetype_name=jira_instance.default_issue_type,
summary=jira_summary(obj),
description=jira_description(obj),
component_name=jira_project.component,
custom_fields=jira_project.custom_fields,
labels=labels,
environment=jira_environment(obj),
priority_name=jira_priority(obj),
epic_name_field=get_epic_name_field_name(jira_instance),
duedate=duedate,
issuetype_fields=issuetype_fields,
default_assignee=jira_project.default_assignee)
except TemplateDoesNotExist as e:
message = f"Failed to find a jira issue template to be used - {e}"
return failure_to_add_message(message, e, obj)
except Exception as e:
message = f"Failed to fetch fields for {jira_instance.default_issue_type} under project {jira_project.project_key} - {e}"
return failure_to_add_message(message, e, obj)
# Create a new issue in Jira with the fields set in the last step
try:
logger.debug("sending fields to JIRA: %s", fields)
new_issue = jira.create_issue(fields)
logger.debug("saving JIRA_Issue for %s finding %s", new_issue.key, obj.id)
j_issue = JIRA_Issue(jira_id=new_issue.id, jira_key=new_issue.key, jira_project=jira_project)
j_issue.set_obj(obj)
j_issue.jira_creation = timezone.now()
j_issue.jira_change = timezone.now()
j_issue.save()
jira.issue(new_issue.id)
logger.info("Created the following jira issue for %d:%s", obj.id, to_str_typed(obj))
except Exception as e:
message = f"Failed to create jira issue with the following payload: {fields} - {e}"
return failure_to_add_message(message, e, obj)
# Attempt to set a default assignee
try:
if jira_project.default_assignee:
created_assignee = str(new_issue.get_field("assignee"))
logger.debug("new issue created with assignee %s", created_assignee)
if created_assignee != jira_project.default_assignee:
jira.assign_issue(new_issue.key, jira_project.default_assignee)
except Exception as e:
message = f"Failed to assign the default user: {jira_project.default_assignee} - {e}"
# Do not return here as this should be a soft failure that should be logged
failure_to_add_message(message, e, obj)
# Upload dojo finding screenshots to Jira
try:
findings = [obj]
if isinstance(obj, Finding_Group):
findings = obj.findings.all()
for find in findings:
for pic in get_file_images(find):
# It doesn't look like the celery cotainer has anything in the media
# folder. Has this feature ever worked?
try:
jira_attachment(
find, jira, new_issue,
settings.MEDIA_ROOT + "/" + pic)
except FileNotFoundError as e:
logger.info(e)
except Exception as e:
message = f"Failed to attach attachments to the jira issue: {e}"
# Do not return here as this should be a soft failure that should be logged
failure_to_add_message(message, e, obj)
# Add any notes that already exist in the finding to the JIRA
try:
for find in findings:
if find.notes.all():
for note in find.notes.all().reverse():
add_comment(obj, note)
except Exception as e:
message = f"Failed to add notes to the jira ticket: {e}"
# Do not return here as this should be a soft failure that should be logged
failure_to_add_message(message, e, obj)
# Determine whether to assign this new jira issue to a mapped epic
try:
if jira_project.enable_engagement_epic_mapping:
eng = obj.test.engagement
logger.debug("Adding to EPIC Map: %s", eng.name)
epic = get_jira_issue(eng)
if epic:
add_issues_to_epic(jira, obj, epic_id=epic.jira_id, issue_keys=[str(new_issue.id)], ignore_epics=True)
else:
logger.info("The following EPIC does not exist: %s", eng.name)
except Exception as e:
message = f"Failed to assign jira issue to existing epic: {e}"
return failure_to_add_message(message, e, obj)
return True
# we need two separate celery tasks due to the decorators we're using to map to/from ids
@dojo_model_to_id
@dojo_async_task
@app.task
@dojo_model_from_id
def update_jira_issue_for_finding(finding, *args, **kwargs):
return update_jira_issue(finding, *args, **kwargs)
@dojo_model_to_id
@dojo_async_task
@app.task
@dojo_model_from_id(model=Finding_Group)
def update_jira_issue_for_finding_group(finding_group, *args, **kwargs):
return update_jira_issue(finding_group, *args, **kwargs)
def update_jira_issue(obj, *args, **kwargs):
def failure_to_update_message(message: str, exception: Exception, obj: Any) -> bool:
if exception:
logger.exception(exception)
logger.error(message)
log_jira_alert(message, obj)
return False
logger.debug("trying to update a linked jira issue for %d:%s", obj.id, to_str_typed(obj))
if not is_jira_enabled():
return False
jira_project = get_jira_project(obj)
jira_instance = get_jira_instance(obj)
if not is_jira_configured_and_enabled(obj):
message = f"Object {obj.id} cannot be pushed to JIRA as there is no JIRA configuration for {to_str_typed(obj)}."
return failure_to_update_message(message, None, obj)
j_issue = obj.jira_issue
try:
JIRAError.log_to_tempfile = False
jira = get_jira_connection(jira_instance)
issue = jira.issue(j_issue.jira_id)
except Exception as e:
message = f"The following jira instance could not be connected: {jira_instance} - {e}"
return failure_to_update_message(message, e, obj)
# Set the list of labels to set on the jira issue
labels = get_labels(obj) + get_tags(obj)
if labels:
labels = list(dict.fromkeys(labels)) # de-dup
# Set the fields that will compose the jira issue
try:
issuetype_fields = get_issuetype_fields(jira, jira_project.project_key, jira_instance.default_issue_type)
fields = prepare_jira_issue_fields(
project_key=jira_project.project_key,
issuetype_name=jira_instance.default_issue_type,
summary=jira_summary(obj),
description=jira_description(obj),
component_name=jira_project.component if not issue.fields.components else None,
labels=labels + issue.fields.labels,
environment=jira_environment(obj),
# Do not update the priority in jira after creation as this could have changed in jira, but should not change in dojo
# priority_name=jira_priority(obj),
issuetype_fields=issuetype_fields)
except Exception as e:
message = f"Failed to fetch fields for {jira_instance.default_issue_type} under project {jira_project.project_key} - {e}"
return failure_to_update_message(message, e, obj)
# Update the issue in jira
try:
logger.debug("sending fields to JIRA: %s", fields)
issue.update(
summary=fields["summary"],
description=fields["description"],
# Do not update the priority in jira after creation as this could have changed in jira, but should not change in dojo
# priority=fields['priority'],
fields=fields)
j_issue.jira_change = timezone.now()
j_issue.save()
except Exception as e:
message = f"Failed to update the jira issue with the following payload: {fields} - {e}"
return failure_to_update_message(message, e, obj)
# Update the status in jira
try:
push_status_to_jira(obj, jira_instance, jira, issue)
except Exception as e:
message = f"Failed to update the jira issue status - {e}"
return failure_to_update_message(message, e, obj)
# Upload dojo finding screenshots to Jira
try:
findings = [obj]
if isinstance(obj, Finding_Group):
findings = obj.findings.all()
for find in findings:
for pic in get_file_images(find):
# It doesn't look like the celery container has anything in the media
# folder. Has this feature ever worked?
try:
jira_attachment(
find, jira, issue,
settings.MEDIA_ROOT + "/" + pic)
except FileNotFoundError as e: