10
10
from prowler .lib .outputs .finding import Finding
11
11
from prowler .lib .outputs .jira .exceptions .exceptions import (
12
12
JiraAuthenticationError ,
13
+ JiraBasicAuthError ,
13
14
JiraCreateIssueError ,
14
15
JiraGetAccessTokenError ,
15
16
JiraGetAuthResponseError ,
21
22
JiraGetProjectsError ,
22
23
JiraGetProjectsResponseError ,
23
24
JiraInvalidIssueTypeError ,
25
+ JiraInvalidParameterError ,
24
26
JiraInvalidProjectKeyError ,
25
27
JiraNoProjectsError ,
26
28
JiraNoTokenError ,
@@ -84,6 +86,8 @@ class Jira:
84
86
- JiraCreateIssueError: Failed to create an issue in Jira
85
87
- JiraSendFindingsResponseError: Failed to send the findings to Jira
86
88
- JiraTestConnectionError: Failed to test the connection
89
+ - JiraBasicAuthError: Failed to authenticate using basic auth
90
+ - JiraInvalidParameterError: The provided parameters in Init are invalid
87
91
88
92
Usage:
89
93
jira = Jira(
@@ -98,6 +102,10 @@ class Jira:
98
102
_client_id : str = None
99
103
_client_secret : str = None
100
104
_access_token : str = None
105
+ _user_mail : str = None
106
+ _api_token : str = None
107
+ _domain : str = None
108
+ _using_basic_auth : bool = False
101
109
_refresh_token : str = None
102
110
_expiration_date : int = None
103
111
_cloud_id : str = None
@@ -120,14 +128,31 @@ def __init__(
120
128
redirect_uri : str = None ,
121
129
client_id : str = None ,
122
130
client_secret : str = None ,
131
+ user_mail : str = None ,
132
+ api_token : str = None ,
133
+ domain : str = None ,
123
134
):
124
135
self ._redirect_uri = redirect_uri
125
136
self ._client_id = client_id
126
137
self ._client_secret = client_secret
138
+ self ._user_mail = user_mail
139
+ self ._api_token = api_token
140
+ self ._domain = domain
127
141
self ._scopes = ["read:jira-user" , "read:jira-work" , "write:jira-work" ]
128
- auth_url = self .auth_code_url ()
129
- authorization_code = self .input_authorization_code (auth_url )
130
- self .get_auth (authorization_code )
142
+ # If the client mail, API token and site name are present, use basic auth
143
+ if user_mail and api_token and domain :
144
+ self ._using_basic_auth = True
145
+ self .get_basic_auth ()
146
+ # If the redirect URI, client ID and client secret are present, use auth code flow
147
+ elif redirect_uri and client_id and client_secret :
148
+ auth_url = self .auth_code_url ()
149
+ authorization_code = self .input_authorization_code (auth_url )
150
+ self .get_auth (authorization_code )
151
+ else :
152
+ init_error = "Failed to initialize Jira object, missing parameters."
153
+ raise JiraInvalidParameterError (
154
+ message = init_error , file = os .path .basename (__file__ )
155
+ )
131
156
132
157
@property
133
158
def redirect_uri (self ):
@@ -157,6 +182,10 @@ def cloud_id(self, value):
157
182
def scopes (self ):
158
183
return self ._scopes
159
184
185
+ @property
186
+ def using_basic_auth (self ):
187
+ return self ._using_basic_auth
188
+
160
189
def get_params (self , state_encoded ):
161
190
return {
162
191
** self .PARAMS_TEMPLATE ,
@@ -209,6 +238,29 @@ def get_timestamp_from_seconds(seconds: int) -> datetime:
209
238
"""
210
239
return (datetime .now () + timedelta (seconds = seconds )).isoformat ()
211
240
241
+ def get_basic_auth (self ) -> None :
242
+ """Get the access token using the mail and API token.
243
+
244
+ Returns:
245
+ - None
246
+
247
+ Raises:
248
+ - JiraBasicAuthError: Failed to authenticate using basic auth
249
+ """
250
+ try :
251
+ user_string = f"{ self ._user_mail } :{ self ._api_token } "
252
+ self ._access_token = base64 .b64encode (user_string .encode ("utf-8" )).decode (
253
+ "utf-8"
254
+ )
255
+ self ._cloud_id = self .get_cloud_id (self ._access_token , domain = self ._domain )
256
+ except Exception as e :
257
+ message_error = f"Failed to get auth using basic auth: { e } "
258
+ logger .error (message_error )
259
+ raise JiraBasicAuthError (
260
+ message = message_error ,
261
+ file = os .path .basename (__file__ ),
262
+ )
263
+
212
264
def get_auth (self , auth_code : str = None ) -> None :
213
265
"""Get the access token and refresh token
214
266
@@ -269,11 +321,12 @@ def get_auth(self, auth_code: str = None) -> None:
269
321
file = os .path .basename (__file__ ),
270
322
)
271
323
272
- def get_cloud_id (self , access_token : str = None ) -> str :
324
+ def get_cloud_id (self , access_token : str = None , domain : str = None ) -> str :
273
325
"""Get the cloud ID from Jira
274
326
275
327
Args:
276
328
- access_token: The access token from Jira
329
+ - domain: The site name from Jira
277
330
278
331
Returns:
279
332
- str: The cloud ID
@@ -284,8 +337,17 @@ def get_cloud_id(self, access_token: str = None) -> str:
284
337
- JiraGetCloudIDError: Failed to get the cloud ID from Jira
285
338
"""
286
339
try :
287
- headers = {"Authorization" : f"Bearer { access_token } " }
288
- response = requests .get (self .API_TOKEN_URL , headers = headers )
340
+ if self ._using_basic_auth :
341
+ headers = {"Authorization" : f"Basic { access_token } " }
342
+ response = requests .get (
343
+ f"https://{ domain } .atlassian.net/_edge/tenant_info" ,
344
+ headers = headers ,
345
+ )
346
+ response = response .json ()
347
+ return response .get ("cloudId" )
348
+ else :
349
+ headers = {"Authorization" : f"Bearer { access_token } " }
350
+ response = requests .get (self .API_TOKEN_URL , headers = headers )
289
351
290
352
if response .status_code == 200 :
291
353
resources = response .json ()
@@ -326,6 +388,10 @@ def get_access_token(self) -> str:
326
388
- JiraGetAccessTokenError: Failed to get the access token
327
389
"""
328
390
try :
391
+ # If using basic auth, return the access token
392
+ if self ._using_basic_auth :
393
+ return self ._access_token
394
+
329
395
if self .auth_expiration and datetime .now () < datetime .fromisoformat (
330
396
self .auth_expiration
331
397
):
@@ -392,6 +458,9 @@ def test_connection(
392
458
redirect_uri : str = None ,
393
459
client_id : str = None ,
394
460
client_secret : str = None ,
461
+ user_mail : str = None ,
462
+ api_token : str = None ,
463
+ domain : str = None ,
395
464
raise_on_exception : bool = True ,
396
465
) -> Connection :
397
466
"""Test the connection to Jira
@@ -400,6 +469,9 @@ def test_connection(
400
469
- redirect_uri: The redirect URI
401
470
- client_id: The client ID
402
471
- client_secret: The client secret
472
+ - user_mail: The client mail
473
+ - api_token: The API token
474
+ - domain: The site name
403
475
- raise_on_exception: Whether to raise an exception or not
404
476
405
477
Returns:
@@ -417,13 +489,20 @@ def test_connection(
417
489
redirect_uri = redirect_uri ,
418
490
client_id = client_id ,
419
491
client_secret = client_secret ,
492
+ user_mail = user_mail ,
493
+ api_token = api_token ,
494
+ domain = domain ,
420
495
)
421
496
access_token = jira .get_access_token ()
422
497
423
498
if not access_token :
424
499
return ValueError ("Failed to get access token" )
425
500
426
- headers = {"Authorization" : f"Bearer { access_token } " }
501
+ if jira .using_basic_auth :
502
+ headers = {"Authorization" : f"Basic { access_token } " }
503
+ else :
504
+ headers = {"Authorization" : f"Bearer { access_token } " }
505
+
427
506
response = requests .get (
428
507
f"https://api.atlassian.com/ex/jira/{ jira .cloud_id } /rest/api/3/myself" ,
429
508
headers = headers ,
@@ -461,6 +540,13 @@ def test_connection(
461
540
if raise_on_exception :
462
541
raise auth_error
463
542
return Connection (error = auth_error )
543
+ except JiraBasicAuthError as basic_auth_error :
544
+ logger .error (
545
+ f"{ basic_auth_error .__class__ .__name__ } [{ basic_auth_error .__traceback__ .tb_lineno } ]: { basic_auth_error } "
546
+ )
547
+ if raise_on_exception :
548
+ raise basic_auth_error
549
+ return Connection (error = basic_auth_error )
464
550
except Exception as error :
465
551
logger .error (f"Failed to test connection: { error } " )
466
552
if raise_on_exception :
@@ -489,7 +575,11 @@ def get_projects(self) -> Dict[str, str]:
489
575
if not access_token :
490
576
return ValueError ("Failed to get access token" )
491
577
492
- headers = {"Authorization" : f"Bearer { access_token } " }
578
+ if self ._using_basic_auth :
579
+ headers = {"Authorization" : f"Basic { access_token } " }
580
+ else :
581
+ headers = {"Authorization" : f"Bearer { access_token } " }
582
+
493
583
response = requests .get (
494
584
f"https://api.atlassian.com/ex/jira/{ self .cloud_id } /rest/api/3/project" ,
495
585
headers = headers ,
@@ -500,7 +590,7 @@ def get_projects(self) -> Dict[str, str]:
500
590
projects = {
501
591
project ["key" ]: project ["name" ] for project in response .json ()
502
592
}
503
- if len ( projects ) == 0 :
593
+ if projects == {}: # If no projects are found
504
594
logger .error ("No projects found" )
505
595
raise JiraNoProjectsError (
506
596
message = "No projects found in Jira" ,
@@ -555,7 +645,11 @@ def get_available_issue_types(self, project_key: str = None) -> list[str]:
555
645
file = os .path .basename (__file__ ),
556
646
)
557
647
558
- headers = {"Authorization" : f"Bearer { access_token } " }
648
+ if self ._using_basic_auth :
649
+ headers = {"Authorization" : f"Basic { access_token } " }
650
+ else :
651
+ headers = {"Authorization" : f"Bearer { access_token } " }
652
+
559
653
response = requests .get (
560
654
f"https://api.atlassian.com/ex/jira/{ self .cloud_id } /rest/api/3/issue/createmeta?projectKeys={ project_key } &expand=projects.issuetypes.fields" ,
561
655
headers = headers ,
@@ -1109,10 +1203,17 @@ def send_findings(
1109
1203
raise JiraInvalidIssueTypeError (
1110
1204
message = "The issue type is invalid" , file = os .path .basename (__file__ )
1111
1205
)
1112
- headers = {
1113
- "Authorization" : f"Bearer { access_token } " ,
1114
- "Content-Type" : "application/json" ,
1115
- }
1206
+
1207
+ if self ._using_basic_auth :
1208
+ headers = {
1209
+ "Authorization" : f"Basic { access_token } " ,
1210
+ "Content-Type" : "application/json" ,
1211
+ }
1212
+ else :
1213
+ headers = {
1214
+ "Authorization" : f"Bearer { access_token } " ,
1215
+ "Content-Type" : "application/json" ,
1216
+ }
1116
1217
1117
1218
for finding in findings :
1118
1219
status_color = self .get_color_from_status (finding .status .value )
0 commit comments