Skip to content

Commit 64c43a2

Browse files
authored
feat(jira): add force accept language for requests (#8674)
1 parent 74bf0e6 commit 64c43a2

File tree

3 files changed

+142
-36
lines changed

3 files changed

+142
-36
lines changed

prowler/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
1313
- Support color for MANUAL finidngs in Jira tickets [(#8642)](https://github.com/prowler-cloud/prowler/pull/8642)
1414
- `--excluded-checks-file` flag [(#8301)](https://github.com/prowler-cloud/prowler/pull/8301)
1515
- Send finding in Jira integration with the needed values [(#8648)](https://github.com/prowler-cloud/prowler/pull/8648)
16+
- Add language enforcement for Jira requests [(#8674)](https://github.com/prowler-cloud/prowler/pull/8674)
1617
- MongoDB Atlas provider with 10 security checks [(#8312)](https://github.com/prowler-cloud/prowler/pull/8312)
1718
- `clusters_authentication_enabled` - Ensure clusters have authentication enabled
1819
- `clusters_backup_enabled` - Ensure clusters have backup enabled

prowler/lib/outputs/jira/jira.py

Lines changed: 39 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,11 @@ class Jira:
136136
}
137137
TOKEN_URL = "https://auth.atlassian.com/oauth/token"
138138
API_TOKEN_URL = "https://api.atlassian.com/oauth/token/accessible-resources"
139+
HEADER_TEMPLATE = {
140+
"Content-Type": "application/json",
141+
"X-Force-Accept-Language": "true",
142+
"Accept-Language": "en",
143+
}
139144

140145
def __init__(
141146
self,
@@ -200,6 +205,31 @@ def scopes(self):
200205
def using_basic_auth(self):
201206
return self._using_basic_auth
202207

208+
def get_headers(
209+
self, access_token: str = None, content_type_json: bool = False
210+
) -> dict:
211+
"""Get headers for API requests
212+
213+
Args:
214+
access_token: The access token to use for authorization
215+
content_type_json: Whether to include Content-Type: application/json
216+
217+
Returns:
218+
dict: Headers for API requests
219+
"""
220+
headers = self.HEADER_TEMPLATE.copy()
221+
222+
if not content_type_json:
223+
headers.pop("Content-Type", None)
224+
225+
if access_token:
226+
if self._using_basic_auth:
227+
headers["Authorization"] = f"Basic {access_token}"
228+
else:
229+
headers["Authorization"] = f"Bearer {access_token}"
230+
231+
return headers
232+
203233
def get_params(self, state_encoded):
204234
return {
205235
**self.PARAMS_TEMPLATE,
@@ -303,7 +333,7 @@ def get_auth(self, auth_code: str = None) -> None:
303333
"redirect_uri": self.redirect_uri,
304334
}
305335

306-
headers = {"Content-Type": "application/json"}
336+
headers = self.get_headers(content_type_json=True)
307337
response = requests.post(self.TOKEN_URL, json=body, headers=headers)
308338

309339
if response.status_code == 200:
@@ -352,15 +382,15 @@ def get_cloud_id(self, access_token: str = None, domain: str = None) -> str:
352382
"""
353383
try:
354384
if self._using_basic_auth:
355-
headers = {"Authorization": f"Basic {access_token}"}
385+
headers = self.get_headers(access_token)
356386
response = requests.get(
357387
f"https://{domain}.atlassian.net/_edge/tenant_info",
358388
headers=headers,
359389
)
360390
response = response.json()
361391
return response.get("cloudId")
362392
else:
363-
headers = {"Authorization": f"Bearer {access_token}"}
393+
headers = self.get_headers(access_token)
364394
response = requests.get(self.API_TOKEN_URL, headers=headers)
365395

366396
if response.status_code == 200:
@@ -442,7 +472,7 @@ def refresh_access_token(self) -> str:
442472
"refresh_token": self._refresh_token,
443473
}
444474

445-
headers = {"Content-Type": "application/json"}
475+
headers = self.get_headers(content_type_json=True)
446476
response = requests.post(url, json=body, headers=headers)
447477

448478
if response.status_code == 200:
@@ -582,10 +612,7 @@ def get_projects(self) -> Dict[str, str]:
582612
if not access_token:
583613
return ValueError("Failed to get access token")
584614

585-
if self._using_basic_auth:
586-
headers = {"Authorization": f"Basic {access_token}"}
587-
else:
588-
headers = {"Authorization": f"Bearer {access_token}"}
615+
headers = self.get_headers(access_token)
589616

590617
response = requests.get(
591618
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/project",
@@ -652,10 +679,7 @@ def get_available_issue_types(self, project_key: str = None) -> list[str]:
652679
file=os.path.basename(__file__),
653680
)
654681

655-
if self._using_basic_auth:
656-
headers = {"Authorization": f"Basic {access_token}"}
657-
else:
658-
headers = {"Authorization": f"Bearer {access_token}"}
682+
headers = self.get_headers(access_token)
659683

660684
response = requests.get(
661685
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/issue/createmeta?projectKeys={project_key}&expand=projects.issuetypes.fields",
@@ -700,10 +724,7 @@ def get_metadata(self) -> dict:
700724
if not access_token:
701725
return ValueError("Failed to get access token")
702726

703-
if self._using_basic_auth:
704-
headers = {"Authorization": f"Basic {access_token}"}
705-
else:
706-
headers = {"Authorization": f"Bearer {access_token}"}
727+
headers = self.get_headers(access_token)
707728

708729
response = requests.get(
709730
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/project",
@@ -1579,16 +1600,7 @@ def send_findings(
15791600
message="The issue type is invalid", file=os.path.basename(__file__)
15801601
)
15811602

1582-
if self._using_basic_auth:
1583-
headers = {
1584-
"Authorization": f"Basic {access_token}",
1585-
"Content-Type": "application/json",
1586-
}
1587-
else:
1588-
headers = {
1589-
"Authorization": f"Bearer {access_token}",
1590-
"Content-Type": "application/json",
1591-
}
1603+
headers = self.get_headers(access_token, content_type_json=True)
15921604

15931605
for finding in findings:
15941606
status_color = self.get_color_from_status(finding.status.value)
@@ -1795,16 +1807,7 @@ def send_finding(
17951807
message="The issue type is invalid", file=os.path.basename(__file__)
17961808
)
17971809

1798-
if self._using_basic_auth:
1799-
headers = {
1800-
"Authorization": f"Basic {access_token}",
1801-
"Content-Type": "application/json",
1802-
}
1803-
else:
1804-
headers = {
1805-
"Authorization": f"Bearer {access_token}",
1806-
"Content-Type": "application/json",
1807-
}
1810+
headers = self.get_headers(access_token, content_type_json=True)
18081811

18091812
status_color = self.get_color_from_status(status)
18101813
severity_color = self.get_severity_color(severity.lower())

tests/lib/outputs/jira/jira_test.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,8 @@ def test_send_findings_successful(
721721
expected_headers = {
722722
"Authorization": "Bearer valid_access_token",
723723
"Content-Type": "application/json",
724+
"X-Force-Accept-Language": "true",
725+
"Accept-Language": "en",
724726
}
725727

726728
assert call_args[0][0] == expected_url
@@ -1418,3 +1420,103 @@ def test_send_finding_custom_fields_error(
14181420

14191421
assert result is False
14201422
mock_post.assert_called_once()
1423+
1424+
def test_get_headers_oauth_with_access_token(self):
1425+
"""Test get_headers returns correct OAuth headers with access token."""
1426+
self.jira_integration._using_basic_auth = False
1427+
1428+
headers = self.jira_integration.get_headers(
1429+
access_token="test_oauth_token", content_type_json=True
1430+
)
1431+
1432+
expected_headers = {
1433+
"Authorization": "Bearer test_oauth_token",
1434+
"Content-Type": "application/json",
1435+
"X-Force-Accept-Language": "true",
1436+
"Accept-Language": "en",
1437+
}
1438+
assert headers == expected_headers
1439+
1440+
def test_get_headers_oauth_without_content_type(self):
1441+
"""Test get_headers returns OAuth headers without Content-Type when content_type_json=False."""
1442+
self.jira_integration._using_basic_auth = False
1443+
1444+
headers = self.jira_integration.get_headers(
1445+
access_token="test_oauth_token", content_type_json=False
1446+
)
1447+
1448+
expected_headers = {
1449+
"Authorization": "Bearer test_oauth_token",
1450+
"X-Force-Accept-Language": "true",
1451+
"Accept-Language": "en",
1452+
}
1453+
assert headers == expected_headers
1454+
assert "Content-Type" not in headers
1455+
1456+
def test_get_headers_basic_auth_with_access_token(self):
1457+
"""Test get_headers returns correct Basic Auth headers with access token."""
1458+
self.jira_integration_basic_auth._using_basic_auth = True
1459+
1460+
headers = self.jira_integration_basic_auth.get_headers(
1461+
access_token="test_basic_token", content_type_json=True
1462+
)
1463+
1464+
expected_headers = {
1465+
"Authorization": "Basic test_basic_token",
1466+
"Content-Type": "application/json",
1467+
"X-Force-Accept-Language": "true",
1468+
"Accept-Language": "en",
1469+
}
1470+
assert headers == expected_headers
1471+
1472+
def test_get_headers_basic_auth_without_content_type(self):
1473+
"""Test get_headers returns Basic Auth headers without Content-Type when content_type_json=False."""
1474+
self.jira_integration_basic_auth._using_basic_auth = True
1475+
1476+
headers = self.jira_integration_basic_auth.get_headers(
1477+
access_token="test_basic_token", content_type_json=False
1478+
)
1479+
1480+
expected_headers = {
1481+
"Authorization": "Basic test_basic_token",
1482+
"X-Force-Accept-Language": "true",
1483+
"Accept-Language": "en",
1484+
}
1485+
assert headers == expected_headers
1486+
assert "Content-Type" not in headers
1487+
1488+
def test_get_headers_without_access_token_with_content_type(self):
1489+
"""Test get_headers returns headers without Authorization when no access token provided."""
1490+
headers = self.jira_integration.get_headers(content_type_json=True)
1491+
1492+
expected_headers = {
1493+
"Content-Type": "application/json",
1494+
"X-Force-Accept-Language": "true",
1495+
"Accept-Language": "en",
1496+
}
1497+
assert headers == expected_headers
1498+
assert "Authorization" not in headers
1499+
1500+
def test_get_headers_without_access_token_without_content_type(self):
1501+
"""Test get_headers returns minimal headers when no access token and no content type."""
1502+
headers = self.jira_integration.get_headers(content_type_json=False)
1503+
1504+
expected_headers = {
1505+
"X-Force-Accept-Language": "true",
1506+
"Accept-Language": "en",
1507+
}
1508+
assert headers == expected_headers
1509+
assert "Authorization" not in headers
1510+
assert "Content-Type" not in headers
1511+
1512+
def test_get_headers_default_parameters(self):
1513+
"""Test get_headers with default parameters (no access token, no content type)."""
1514+
headers = self.jira_integration.get_headers()
1515+
1516+
expected_headers = {
1517+
"X-Force-Accept-Language": "true",
1518+
"Accept-Language": "en",
1519+
}
1520+
assert headers == expected_headers
1521+
assert "Authorization" not in headers
1522+
assert "Content-Type" not in headers

0 commit comments

Comments
 (0)