@@ -109,12 +109,36 @@ def is_push_all_issues(instance):
109
109
return None
110
110
111
111
112
- def _safely_get_finding_group_status (finding_group : Finding_Group ) -> str :
113
- # Accommodating a strange behavior where a finding group sometimes prefers `obj.status` rather than `obj.status()`
114
- try :
115
- return finding_group .status ()
116
- except TypeError : # TypeError: 'str' object is not callable
117
- return finding_group .status
112
+ def _safely_get_obj_status_for_jira (obj : Finding | Finding_Group , * , isenforced : bool = False ) -> str :
113
+ # Accommodating a strange behavior where a obj sometimes prefers `obj.status` rather than `obj.status()`
114
+ status = []
115
+ if isinstance (obj , Finding ):
116
+ try :
117
+ return obj .status ()
118
+ except TypeError : # TypeError: 'str' object is not callable
119
+ return obj .status
120
+
121
+ if isinstance (obj , Finding_Group ):
122
+ # only consider findings that are above the minimum threshold, but includ inactive and non-verified findings
123
+ findings = get_finding_group_findings_above_threshold (obj )
124
+ if not findings :
125
+ return ["Empty" , "Inactive" ]
126
+
127
+ for find in findings :
128
+ logger .debug (f"Finding { find .id } status { find .active } { find .verified } { find .is_mitigated } " )
129
+
130
+ # This iterates 3 times over the list of findings, but any code doing 1 iteration would looke it's from 1990
131
+ if any (find .active for find in findings ):
132
+ status += ["Active" ]
133
+
134
+ if any ((find .active and find .verified ) for find in findings ):
135
+ status += ["Verified" ]
136
+
137
+ if all (find .is_mitigated for find in findings ):
138
+ status += ["Mitigated" , "Inactive" ]
139
+
140
+ # if no active findings are found, we must assume the status is inactive
141
+ return status or ["Inactive" ]
118
142
119
143
120
144
# checks if a finding can be pushed to JIRA
@@ -141,6 +165,12 @@ def can_be_pushed_to_jira(obj, form=None):
141
165
# findings or groups already having an existing jira issue can always be pushed
142
166
return True , None , None
143
167
168
+ jira_minimum_threshold = None
169
+ if System_Settings .objects .get ().jira_minimum_severity :
170
+ jira_minimum_threshold = Finding .get_number_severity (System_Settings .objects .get ().jira_minimum_severity )
171
+
172
+ isenforced = get_system_setting ("enforce_verified_status" , True ) or get_system_setting ("enforce_verified_status_jira" , True )
173
+
144
174
if isinstance (obj , Finding ):
145
175
if form :
146
176
active = form ["active" ].value ()
@@ -153,25 +183,24 @@ def can_be_pushed_to_jira(obj, form=None):
153
183
154
184
logger .debug ("can_be_pushed_to_jira: %s, %s, %s" , active , verified , severity )
155
185
156
- isenforced = get_system_setting ("enforce_verified_status" , True ) or get_system_setting ("enforce_verified_status_jira" , True )
157
-
158
186
if not active or (not verified and isenforced ):
159
187
logger .debug ("Findings must be active and verified, if enforced by system settings, to be pushed to JIRA" )
160
- return False , "Findings must be active and verified, if enforced by system settings, to be pushed to JIRA" , "not_active_or_verified "
188
+ return False , "Findings must be active and verified, if enforced by system settings, to be pushed to JIRA" , "error_not_active_or_verified "
161
189
162
- jira_minimum_threshold = None
163
- if System_Settings .objects .get ().jira_minimum_severity :
164
- jira_minimum_threshold = Finding .get_number_severity (System_Settings .objects .get ().jira_minimum_severity )
165
-
166
- if jira_minimum_threshold and jira_minimum_threshold > Finding .get_number_severity (severity ):
167
- logger .debug (f"Finding below the minimum JIRA severity threshold ({ System_Settings .objects .get ().jira_minimum_severity } )." )
168
- return False , f"Finding below the minimum JIRA severity threshold ({ System_Settings .objects .get ().jira_minimum_severity } )." , "below_minimum_threshold"
190
+ if jira_minimum_threshold and jira_minimum_threshold > Finding .get_number_severity (severity ):
191
+ logger .debug (f"Finding below the minimum JIRA severity threshold ({ System_Settings .objects .get ().jira_minimum_severity } )." )
192
+ return False , f"Finding below the minimum JIRA severity threshold ({ System_Settings .objects .get ().jira_minimum_severity } )." , "error_below_minimum_threshold"
169
193
elif isinstance (obj , Finding_Group ):
170
- if not obj .findings .all ():
171
- return False , f"{ to_str_typed (obj )} cannot be pushed to jira as it is empty." , "error_empty"
172
- # Determine if the finding group is not active
173
- if "Active" not in _safely_get_finding_group_status (obj ):
174
- return False , f"{ to_str_typed (obj )} cannot be pushed to jira as it is not active." , "error_inactive"
194
+ finding_group_status = _safely_get_obj_status_for_jira (obj )
195
+ logger .error (f"Finding group status: { finding_group_status } " )
196
+ if "Empty" in finding_group_status :
197
+ return False , f"{ to_str_typed (obj )} cannot be pushed to jira as it contains no findings above minimum treshold." , "error_empty"
198
+
199
+ if isenforced and "Verified" not in finding_group_status :
200
+ return False , f"{ to_str_typed (obj )} cannot be pushed to jira as it contains no active and verified findings above minimum treshold." , "error_not_active_or_verified"
201
+
202
+ if "Active" not in _safely_get_obj_status_for_jira (obj ):
203
+ return False , f"{ to_str_typed (obj )} cannot be pushed to jira as it contains no active findings above minimum treshold." , "error_inactive"
175
204
176
205
else :
177
206
return False , f"{ to_str_typed (obj )} cannot be pushed to jira as it is of unsupported type." , "error_unsupported"
@@ -511,6 +540,20 @@ def get_jira_status(finding):
511
540
return None
512
541
513
542
543
+ # Used for unit testing so geting all the connections is manadatory
544
+ def get_jira_priortiy (finding ):
545
+ if finding .has_jira_issue :
546
+ j_issue = finding .jira_issue .jira_id
547
+ elif finding .finding_group and finding .finding_group .has_jira_issue :
548
+ j_issue = finding .finding_group .jira_issue .jira_id
549
+
550
+ if j_issue :
551
+ project = get_jira_project (finding )
552
+ issue = jira_get_issue (project , j_issue )
553
+ return issue .fields .priority
554
+ return None
555
+
556
+
514
557
# Used for unit testing so geting all the connections is manadatory
515
558
def get_jira_comments (finding ):
516
559
if finding .has_jira_issue :
@@ -651,7 +694,22 @@ def jira_description(obj, **kwargs):
651
694
652
695
653
696
def jira_priority (obj ):
654
- return get_jira_instance (obj ).get_priority (obj .severity )
697
+ if isinstance (obj , Finding ):
698
+ return get_jira_instance (obj ).get_priority (obj .severity )
699
+
700
+ if isinstance (obj , Finding_Group ):
701
+ # priority based on qualified findings, so if alls criticals get closed, the priority will gets lowered etc
702
+ active_findings = get_qualified_findings (obj )
703
+
704
+ if not active_findings :
705
+ # using a string literal "Info" as we don't really have a "enum" for this anywhere
706
+ max_number_severity = Finding .get_number_severity ("Info" )
707
+ else :
708
+ max_number_severity = max (Finding .get_number_severity (find .severity ) for find in active_findings )
709
+ return get_jira_instance (obj ).get_priority (Finding .get_severity (max_number_severity ))
710
+
711
+ msg = f"Unsupported object type for jira_priority: { obj .__class__ .__name__ } "
712
+ raise ValueError (msg )
655
713
656
714
657
715
def jira_environment (obj ):
@@ -798,7 +856,7 @@ def prepare_jira_issue_fields(
798
856
def add_jira_issue (obj , * args , ** kwargs ):
799
857
def failure_to_add_message (message : str , exception : Exception , _ : Any ) -> bool :
800
858
if exception :
801
- logger .error (exception )
859
+ logger .error ("Exception occurred" , exc_info = exception )
802
860
logger .error (message )
803
861
log_jira_alert (message , obj )
804
862
return False
@@ -842,7 +900,7 @@ def failure_to_add_message(message: str, exception: Exception, _: Any) -> bool:
842
900
duedate = None
843
901
844
902
if System_Settings .objects .get ().enable_finding_sla :
845
- duedate = obj . sla_deadline ( )
903
+ duedate = get_sla_deadline ( obj )
846
904
# Set the fields that will compose the jira issue
847
905
try :
848
906
issuetype_fields = get_issuetype_fields (jira , jira_project .project_key , jira_instance .default_issue_type )
@@ -868,7 +926,7 @@ def failure_to_add_message(message: str, exception: Exception, _: Any) -> bool:
868
926
return failure_to_add_message (message , e , obj )
869
927
# Create a new issue in Jira with the fields set in the last step
870
928
try :
871
- logger .debug ("sending fields to JIRA: %s" , fields )
929
+ logger .debug ("Creating new JIRA issue with fields : %s" , json . dumps ( fields , indent = 4 ) )
872
930
new_issue = jira .create_issue (fields )
873
931
logger .debug ("saving JIRA_Issue for %s finding %s" , new_issue .key , obj .id )
874
932
j_issue = JIRA_Issue (jira_id = new_issue .id , jira_key = new_issue .key , jira_project = jira_project )
@@ -971,6 +1029,19 @@ def failure_to_update_message(message: str, exception: Exception, obj: Any) -> b
971
1029
labels = get_labels (obj ) + get_tags (obj )
972
1030
if labels :
973
1031
labels = list (dict .fromkeys (labels )) # de-dup
1032
+
1033
+ # Only Finding Groups will have their priority synced on updates.
1034
+ # For Findings we resepect any priority change made in JIRA
1035
+ # https://github.com/DefectDojo/django-DefectDojo/pull/9571 and https://github.com/DefectDojo/django-DefectDojo/pull/12475
1036
+ jira_priority_name = None
1037
+ if isinstance (obj , Finding_Group ):
1038
+ jira_priority_name = jira_priority (obj )
1039
+
1040
+ # Determine what due date to set on the jira issue
1041
+ duedate = None
1042
+ if System_Settings .objects .get ().enable_finding_sla :
1043
+ duedate = get_sla_deadline (obj )
1044
+
974
1045
# Set the fields that will compose the jira issue
975
1046
try :
976
1047
issuetype_fields = get_issuetype_fields (jira , jira_project .project_key , jira_instance .default_issue_type )
@@ -982,20 +1053,19 @@ def failure_to_update_message(message: str, exception: Exception, obj: Any) -> b
982
1053
component_name = jira_project .component if not issue .fields .components else None ,
983
1054
labels = labels + issue .fields .labels ,
984
1055
environment = jira_environment (obj ),
985
- # Do not update the priority in jira after creation as this could have changed in jira, but should not change in dojo
986
- # priority_name=jira_priority(obj) ,
1056
+ priority_name = jira_priority_name ,
1057
+ duedate = duedate ,
987
1058
issuetype_fields = issuetype_fields )
988
1059
except Exception as e :
989
1060
message = f"Failed to fetch fields for { jira_instance .default_issue_type } under project { jira_project .project_key } - { e } "
990
1061
return failure_to_update_message (message , e , obj )
1062
+
991
1063
# Update the issue in jira
992
1064
try :
993
- logger .debug ("sending fields to JIRA : %s" , fields )
1065
+ logger .debug ("Updating JIRA issue with fields : %s" , json . dumps ( fields , indent = 4 ) )
994
1066
issue .update (
995
1067
summary = fields ["summary" ],
996
1068
description = fields ["description" ],
997
- # Do not update the priority in jira after creation as this could have changed in jira, but should not change in dojo
998
- # priority=fields['priority'],
999
1069
fields = fields )
1000
1070
j_issue .jira_change = timezone .now ()
1001
1071
j_issue .save ()
@@ -1100,10 +1170,12 @@ def issue_from_jira_is_active(issue_from_jira):
1100
1170
1101
1171
1102
1172
def push_status_to_jira (obj , jira_instance , jira , issue , * , save = False ):
1103
- status_list = _safely_get_finding_group_status (obj )
1173
+ status_list = _safely_get_obj_status_for_jira (obj )
1104
1174
issue_closed = False
1175
+ updated = False
1176
+ logger .debug ("pushing status to JIRA for %d:%s status:%s" , obj .id , to_str_typed (obj ), status_list )
1105
1177
# check RESOLVED_STATUS first to avoid corner cases with findings that are Inactive, but verified
1106
- if any (item in status_list for item in RESOLVED_STATUS ):
1178
+ if not updated and any (item in status_list for item in RESOLVED_STATUS ):
1107
1179
if issue_from_jira_is_active (issue ):
1108
1180
logger .debug ("Transitioning Jira issue to Resolved" )
1109
1181
updated = jira_transition (jira , issue , jira_instance .close_status_key )
@@ -1148,12 +1220,14 @@ def get_issuetype_fields(
1148
1220
try :
1149
1221
project = meta ["projects" ][0 ]
1150
1222
except Exception :
1223
+ logger .debug ("JIRA meta: %s" , json .dumps (meta , indent = 4 )) # this is None safe
1151
1224
msg = "Project misconfigured or no permissions in Jira ?"
1152
1225
raise JIRAError (msg )
1153
1226
1154
1227
try :
1155
1228
issuetype_fields = project ["issuetypes" ][0 ]["fields" ].keys ()
1156
1229
except Exception :
1230
+ logger .debug ("JIRA meta: %s" , json .dumps (meta , indent = 4 )) # this is None safe
1157
1231
msg = "Misconfigured default issue type ?"
1158
1232
raise JIRAError (msg )
1159
1233
@@ -1760,3 +1834,59 @@ def save_and_push_to_jira(finding):
1760
1834
# the updated data of the finding is pushed as part of the group
1761
1835
if push_to_jira_decision and finding_in_group :
1762
1836
push_to_jira (finding .finding_group )
1837
+
1838
+
1839
+ def get_finding_group_findings_above_threshold (finding_group ):
1840
+ """Get the findings that are above the minimum threshold"""
1841
+ jira_minimum_threshold = 0
1842
+ if System_Settings .objects .get ().jira_minimum_severity :
1843
+ jira_minimum_threshold = Finding .get_numerical_severity (System_Settings .objects .get ().jira_minimum_severity )
1844
+
1845
+ return [finding for finding in finding_group .findings .all () if finding .numerical_severity <= jira_minimum_threshold ]
1846
+
1847
+
1848
+ def is_qualified (finding ):
1849
+ """Check if the finding is qualified to be pushed to JIRA, i.e. active, verified (unless not enforced) and severity is above the threshold"""
1850
+ jira_minimum_threshold = None
1851
+ if System_Settings .objects .get ().jira_minimum_severity :
1852
+ jira_minimum_threshold = Finding .get_numerical_severity (System_Settings .objects .get ().jira_minimum_severity )
1853
+
1854
+ isenforced = get_system_setting ("enforce_verified_status" , True ) or get_system_setting ("enforce_verified_status_jira" , True )
1855
+
1856
+ return finding .active and (finding .verified or not isenforced ) and (finding .numerical_severity <= jira_minimum_threshold )
1857
+
1858
+
1859
+ def get_qualified_findings (finding_group ):
1860
+ """Filters findings to return only findings qualified to be pushed to JIRA, i.e. active, verified (unless not enforced) and severity is above the threshold"""
1861
+ if not finding_group .findings .all ():
1862
+ return None
1863
+
1864
+ return [find for find in finding_group .findings .all () if is_qualified (find )]
1865
+
1866
+
1867
+ def get_non_qualified_findings (finding_group ):
1868
+ """Filters findings to return only findings not qualified to be pushed to JIRA, i.e. inactive, not-verified (unless not enforced) and severity is below the threshold"""
1869
+ if not finding_group .findings .all ():
1870
+ return None
1871
+
1872
+ return [find for find in finding_group .findings .all () if not is_qualified (find )]
1873
+
1874
+
1875
+ def get_sla_deadline (obj ):
1876
+ """Get the earliest SLA deadline from a finding or a list of findings, this typically includes all qualified findings in the group"""
1877
+ if not obj :
1878
+ return None
1879
+
1880
+ if isinstance (obj , Finding ):
1881
+ return obj .sla_deadline ()
1882
+
1883
+ if isinstance (obj , Finding_Group ):
1884
+ return min ([find .sla_deadline () for find in get_qualified_findings (obj ) if find .sla_deadline ()], default = None )
1885
+
1886
+ msg = f"get_sla_deadline: obj passed that is not a Finding or Finding_Group: { type (obj )} "
1887
+ raise ValueError (msg )
1888
+
1889
+
1890
+ def get_severity (findings ):
1891
+ max_number_severity = max (Finding .get_number_severity (find .severity ) for find in findings )
1892
+ return Finding .get_severity (max_number_severity )
0 commit comments