@@ -109,12 +109,36 @@ def is_push_all_issues(instance):
109109 return None
110110
111111
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" ]
118142
119143
120144# checks if a finding can be pushed to JIRA
@@ -141,6 +165,12 @@ def can_be_pushed_to_jira(obj, form=None):
141165 # findings or groups already having an existing jira issue can always be pushed
142166 return True , None , None
143167
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+
144174 if isinstance (obj , Finding ):
145175 if form :
146176 active = form ["active" ].value ()
@@ -153,25 +183,24 @@ def can_be_pushed_to_jira(obj, form=None):
153183
154184 logger .debug ("can_be_pushed_to_jira: %s, %s, %s" , active , verified , severity )
155185
156- isenforced = get_system_setting ("enforce_verified_status" , True ) or get_system_setting ("enforce_verified_status_jira" , True )
157-
158186 if not active or (not verified and isenforced ):
159187 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 "
161189
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"
169193 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"
175204
176205 else :
177206 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):
511540 return None
512541
513542
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+
514557# Used for unit testing so geting all the connections is manadatory
515558def get_jira_comments (finding ):
516559 if finding .has_jira_issue :
@@ -651,7 +694,22 @@ def jira_description(obj, **kwargs):
651694
652695
653696def 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 )
655713
656714
657715def jira_environment (obj ):
@@ -798,7 +856,7 @@ def prepare_jira_issue_fields(
798856def add_jira_issue (obj , * args , ** kwargs ):
799857 def failure_to_add_message (message : str , exception : Exception , _ : Any ) -> bool :
800858 if exception :
801- logger .error (exception )
859+ logger .error ("Exception occurred" , exc_info = exception )
802860 logger .error (message )
803861 log_jira_alert (message , obj )
804862 return False
@@ -842,7 +900,7 @@ def failure_to_add_message(message: str, exception: Exception, _: Any) -> bool:
842900 duedate = None
843901
844902 if System_Settings .objects .get ().enable_finding_sla :
845- duedate = obj . sla_deadline ( )
903+ duedate = get_sla_deadline ( obj )
846904 # Set the fields that will compose the jira issue
847905 try :
848906 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:
868926 return failure_to_add_message (message , e , obj )
869927 # Create a new issue in Jira with the fields set in the last step
870928 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 ) )
872930 new_issue = jira .create_issue (fields )
873931 logger .debug ("saving JIRA_Issue for %s finding %s" , new_issue .key , obj .id )
874932 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
9711029 labels = get_labels (obj ) + get_tags (obj )
9721030 if labels :
9731031 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+
9741045 # Set the fields that will compose the jira issue
9751046 try :
9761047 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
9821053 component_name = jira_project .component if not issue .fields .components else None ,
9831054 labels = labels + issue .fields .labels ,
9841055 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 ,
9871058 issuetype_fields = issuetype_fields )
9881059 except Exception as e :
9891060 message = f"Failed to fetch fields for { jira_instance .default_issue_type } under project { jira_project .project_key } - { e } "
9901061 return failure_to_update_message (message , e , obj )
1062+
9911063 # Update the issue in jira
9921064 try :
993- logger .debug ("sending fields to JIRA : %s" , fields )
1065+ logger .debug ("Updating JIRA issue with fields : %s" , json . dumps ( fields , indent = 4 ) )
9941066 issue .update (
9951067 summary = fields ["summary" ],
9961068 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'],
9991069 fields = fields )
10001070 j_issue .jira_change = timezone .now ()
10011071 j_issue .save ()
@@ -1100,10 +1170,12 @@ def issue_from_jira_is_active(issue_from_jira):
11001170
11011171
11021172def 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 )
11041174 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 )
11051177 # 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 ):
11071179 if issue_from_jira_is_active (issue ):
11081180 logger .debug ("Transitioning Jira issue to Resolved" )
11091181 updated = jira_transition (jira , issue , jira_instance .close_status_key )
@@ -1148,12 +1220,14 @@ def get_issuetype_fields(
11481220 try :
11491221 project = meta ["projects" ][0 ]
11501222 except Exception :
1223+ logger .debug ("JIRA meta: %s" , json .dumps (meta , indent = 4 )) # this is None safe
11511224 msg = "Project misconfigured or no permissions in Jira ?"
11521225 raise JIRAError (msg )
11531226
11541227 try :
11551228 issuetype_fields = project ["issuetypes" ][0 ]["fields" ].keys ()
11561229 except Exception :
1230+ logger .debug ("JIRA meta: %s" , json .dumps (meta , indent = 4 )) # this is None safe
11571231 msg = "Misconfigured default issue type ?"
11581232 raise JIRAError (msg )
11591233
@@ -1760,3 +1834,59 @@ def save_and_push_to_jira(finding):
17601834 # the updated data of the finding is pushed as part of the group
17611835 if push_to_jira_decision and finding_in_group :
17621836 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