@@ -52,6 +52,45 @@ class SonarQubeApiUpdater:
5252 },
5353 ]
5454
55+ MAPPING_SONARQUBE_HOTSPOT_STATUS_TRANSITION = [
56+ {
57+ "from" : ["TO_REVIEW" ],
58+ "to" : "RESOLVED / FALSE-POSITIVE" ,
59+ "transition" : "REVIEWED" ,
60+ "resolution" : "SAFE" ,
61+ },
62+ {
63+ "from" : ["TO_REVIEW" ],
64+ "to" : "RESOLVED / FIXED" ,
65+ "transition" : "REVIEWED" ,
66+ "resolution" : "FIXED" ,
67+ },
68+ {
69+ "from" : ["TO_REVIEW" ],
70+ "to" : "RESOLVED / WONTFIX" ,
71+ "transition" : "REVIEWED" ,
72+ "resolution" : "ACKNOWLEDGED" ,
73+ },
74+ {
75+ "from" : ["REVIEWED" ],
76+ "to" : "OPEN" ,
77+ "transition" : "TO_REVIEW" ,
78+ "resolution" : None ,
79+ },
80+ {
81+ "from" : ["REVIEWED" ],
82+ "to" : "REOPENED" ,
83+ "transition" : "TO_REVIEW" ,
84+ "resolution" : None ,
85+ },
86+ {
87+ "from" : ["REVIEWED" ],
88+ "to" : "CONFIRMED" ,
89+ "transition" : "TO_REVIEW" ,
90+ "resolution" : None ,
91+ },
92+ ]
93+
5594 @staticmethod
5695 def get_sonarqube_status_for (finding ):
5796 target_status = None
@@ -66,16 +105,22 @@ def get_sonarqube_status_for(finding):
66105 return target_status
67106
68107 def get_sonarqube_required_transitions_for (
69- self , current_status , target_status ,
70- ):
108+ self , current_status , target_status , is_hotspot ):
71109 # If current and target is the same... do nothing
72110 if current_status == target_status :
73111 return None
74112
113+ # Select the appropriate mapping based on issue type
114+ mapping = (
115+ self .MAPPING_SONARQUBE_HOTSPOT_STATUS_TRANSITION
116+ if is_hotspot
117+ else self .MAPPING_SONARQUBE_STATUS_TRANSITION
118+ )
119+
75120 # Check if there is at least one transition from current_status...
76121 if not [
77122 x
78- for x in self . MAPPING_SONARQUBE_STATUS_TRANSITION
123+ for x in mapping
79124 if current_status in x .get ("from" )
80125 ]:
81126 return None
@@ -84,34 +129,46 @@ def get_sonarqube_required_transitions_for(
84129 # can transition to target_status
85130 transitions = [
86131 x
87- for x in self . MAPPING_SONARQUBE_STATUS_TRANSITION
132+ for x in mapping
88133 if target_status == x .get ("to" )
89134 ]
90135 if transitions :
91136 for transition in transitions :
92137 # There is a direct transition from current status...
93138 if current_status in transition .get ("from" ):
94139 t = transition .get ("transition" )
140+ if is_hotspot :
141+ return [{"status" : t , "resolution" : transition .get ("resolution" )}] if t else None
95142 return [t ] if t else None
96143
97- # We have the last transition to get to our target status but there
98- # is no direct transition
99- transitions_result = deque ()
100- transitions_result .appendleft (transitions [0 ].get ("transition" ))
144+ # Handle complex transitions for regular issues
145+ if not is_hotspot :
146+ # We have the last transition to get to our target status but there
147+ # is no direct transition
148+ transitions_result = deque ()
149+ transitions_result .appendleft (transitions [0 ].get ("transition" ))
101150
102- # Find out previous transitions that would finish in any FROM of a
103- # previous to use as target
104- for transition in transitions :
105- for t_from in transition .get ("from" ):
106- possible_transition = (
107- self .get_sonarqube_required_transitions_for (
108- current_status , t_from ,
151+ # Find out previous transitions that would finish in any FROM of a
152+ # previous to use as target
153+ for transition in transitions :
154+ for t_from in transition .get ("from" ):
155+ possible_transition = (
156+ self .get_sonarqube_required_transitions_for (
157+ current_status , t_from , is_hotspot ,
158+ )
109159 )
110- )
111- if possible_transition :
112- transitions_result .extendleft (possible_transition )
113- return list (transitions_result )
114- return None
160+ if possible_transition :
161+ transitions_result .extendleft (possible_transition )
162+ return list (transitions_result )
163+ else :
164+ # SQ code is too complicated for ISSUES, there is no such thing for HOTSPOTS,
165+ # there are only 2 states: TO_REVIEW and REVIEWED
166+ transitions_result = deque ()
167+ transitions_result .appendleft (
168+ {"status" : transitions [0 ].get ("transition" ),
169+ "resolution" : transitions [0 ].get ("resolution" )},
170+ )
171+ return list (transitions_result )
115172 return None
116173
117174 def update_sonarqube_finding (self , finding ):
@@ -128,44 +185,56 @@ def update_sonarqube_finding(self, finding):
128185 # during import
129186
130187 target_status = self .get_sonarqube_status_for (finding )
188+ is_hotspot = sonarqube_issue .type == "SECURITY_HOTSPOT"
131189
132- issue = client .get_issue (sonarqube_issue .key )
133- if (
134- issue
135- ): # Issue could have disappeared in SQ because a previous scan has resolved the issue as fixed
136- if issue .get ("resolution" ):
137- current_status = "{} / {}" .format (
138- issue .get ("status" ), issue .get ("resolution" ),
139- )
140- else :
141- current_status = issue .get ("status" )
190+ issue = client .get_hotspot (sonarqube_issue .key ) if is_hotspot else client .get_issue (sonarqube_issue .key )
191+
192+ # Issue does not exist (could have disappeared in SQ because a previous scan resolved it)
193+ if not issue :
194+ return
195+
196+ if is_hotspot :
197+ current_status = issue .get ("status" )
198+ elif issue .get ("resolution" ):
199+ current_status = "{} / {}" .format (issue .get ("status" ), issue .get ("resolution" ))
200+ else :
201+ current_status = issue .get ("status" )
142202
203+ # Get required transitions
204+ transitions = self .get_sonarqube_required_transitions_for (
205+ current_status , target_status , is_hotspot = is_hotspot ,
206+ )
207+
208+ if not transitions :
143209 logger .debug (
144- f"--> SQ Current status: { current_status } . Current target status: { target_status } " ,
210+ f"There are no transitions between { current_status } and { target_status } for finding ' { finding } ' in SonarQube " ,
145211 )
212+ return
146213
147- transitions = self . get_sonarqube_required_transitions_for (
148- current_status , target_status ,
214+ logger . debug (
215+ f"Updating finding ' { finding } ' transition { current_status } -> { target_status } in SonarQube" ,
149216 )
150- if transitions :
151- logger .info (
152- f"Updating finding '{ finding } ' in SonarQube" ,
153- )
154217
155- for transition in transitions :
156- client .transition_issue (sonarqube_issue .key , transition )
157-
158- # Track Defect Dojo has updated the SonarQube issue
159- Sonarqube_Issue_Transition .objects .create (
160- sonarqube_issue = finding .sonarqube_issue ,
161- # not sure if this is needed, but looks like the original author decided to send display status
162- # to sonarqube we changed Accepted into Risk Accepted, but we change it back to be sure we don't
163- # break the integration
164- finding_status = finding .status ().replace (
165- "Risk Accepted" , "Accepted" ,
166- )
167- if finding .status ()
168- else finding .status (),
169- sonarqube_status = current_status ,
170- transitions = "," .join (transitions ),
171- )
218+ # Apply transitions
219+ for transition in transitions :
220+ if is_hotspot :
221+ client .transition_hotspot (sonarqube_issue .key ,
222+ status = transition ["status" ],
223+ resolution = transition ["resolution" ])
224+ else :
225+ client .transition_issue (sonarqube_issue .key , transition )
226+
227+ # Track that Defect Dojo has updated the SonarQube issue
228+ Sonarqube_Issue_Transition .objects .create (
229+ sonarqube_issue = finding .sonarqube_issue ,
230+ # not sure if this is needed, but looks like the original author decided to send display status
231+ # to sonarqube we changed Accepted into Risk Accepted, but we change it back to be sure we don't
232+ # break the integration
233+ finding_status = finding .status ().replace (
234+ "Risk Accepted" , "Accepted" ,
235+ )
236+ if finding .status ()
237+ else finding .status (),
238+ sonarqube_status = current_status ,
239+ transitions = "," .join (transition ["status" ] if is_hotspot else transition for transition in transitions ),
240+ )
0 commit comments