diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index fe3b6afead..49f0428d2c 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - Support color for MANUAL finidngs in Jira tickets [(#8642)](https://github.com/prowler-cloud/prowler/pull/8642) - `--excluded-checks-file` flag [(#8301)](https://github.com/prowler-cloud/prowler/pull/8301) - Send finding in Jira integration with the needed values [(#8648)](https://github.com/prowler-cloud/prowler/pull/8648) +- Add language enforcement for Jira requests [(#8674)](https://github.com/prowler-cloud/prowler/pull/8674) - MongoDB Atlas provider with 10 security checks [(#8312)](https://github.com/prowler-cloud/prowler/pull/8312) - `clusters_authentication_enabled` - Ensure clusters have authentication enabled - `clusters_backup_enabled` - Ensure clusters have backup enabled diff --git a/prowler/lib/outputs/jira/jira.py b/prowler/lib/outputs/jira/jira.py index 2491da6c31..fa847d5fbc 100644 --- a/prowler/lib/outputs/jira/jira.py +++ b/prowler/lib/outputs/jira/jira.py @@ -136,6 +136,11 @@ class Jira: } TOKEN_URL = "https://auth.atlassian.com/oauth/token" API_TOKEN_URL = "https://api.atlassian.com/oauth/token/accessible-resources" + HEADER_TEMPLATE = { + "Content-Type": "application/json", + "X-Force-Accept-Language": "true", + "Accept-Language": "en", + } def __init__( self, @@ -200,6 +205,31 @@ class Jira: def using_basic_auth(self): return self._using_basic_auth + def get_headers( + self, access_token: str = None, content_type_json: bool = False + ) -> dict: + """Get headers for API requests + + Args: + access_token: The access token to use for authorization + content_type_json: Whether to include Content-Type: application/json + + Returns: + dict: Headers for API requests + """ + headers = self.HEADER_TEMPLATE.copy() + + if not content_type_json: + headers.pop("Content-Type", None) + + if access_token: + if self._using_basic_auth: + headers["Authorization"] = f"Basic {access_token}" + else: + headers["Authorization"] = f"Bearer {access_token}" + + return headers + def get_params(self, state_encoded): return { **self.PARAMS_TEMPLATE, @@ -303,7 +333,7 @@ class Jira: "redirect_uri": self.redirect_uri, } - headers = {"Content-Type": "application/json"} + headers = self.get_headers(content_type_json=True) response = requests.post(self.TOKEN_URL, json=body, headers=headers) if response.status_code == 200: @@ -352,7 +382,7 @@ class Jira: """ try: if self._using_basic_auth: - headers = {"Authorization": f"Basic {access_token}"} + headers = self.get_headers(access_token) response = requests.get( f"https://{domain}.atlassian.net/_edge/tenant_info", headers=headers, @@ -360,7 +390,7 @@ class Jira: response = response.json() return response.get("cloudId") else: - headers = {"Authorization": f"Bearer {access_token}"} + headers = self.get_headers(access_token) response = requests.get(self.API_TOKEN_URL, headers=headers) if response.status_code == 200: @@ -442,7 +472,7 @@ class Jira: "refresh_token": self._refresh_token, } - headers = {"Content-Type": "application/json"} + headers = self.get_headers(content_type_json=True) response = requests.post(url, json=body, headers=headers) if response.status_code == 200: @@ -582,10 +612,7 @@ class Jira: if not access_token: return ValueError("Failed to get access token") - if self._using_basic_auth: - headers = {"Authorization": f"Basic {access_token}"} - else: - headers = {"Authorization": f"Bearer {access_token}"} + headers = self.get_headers(access_token) response = requests.get( f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/project", @@ -652,10 +679,7 @@ class Jira: file=os.path.basename(__file__), ) - if self._using_basic_auth: - headers = {"Authorization": f"Basic {access_token}"} - else: - headers = {"Authorization": f"Bearer {access_token}"} + headers = self.get_headers(access_token) response = requests.get( 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 @@ class Jira: if not access_token: return ValueError("Failed to get access token") - if self._using_basic_auth: - headers = {"Authorization": f"Basic {access_token}"} - else: - headers = {"Authorization": f"Bearer {access_token}"} + headers = self.get_headers(access_token) response = requests.get( f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/project", @@ -1579,16 +1600,7 @@ class Jira: message="The issue type is invalid", file=os.path.basename(__file__) ) - if self._using_basic_auth: - headers = { - "Authorization": f"Basic {access_token}", - "Content-Type": "application/json", - } - else: - headers = { - "Authorization": f"Bearer {access_token}", - "Content-Type": "application/json", - } + headers = self.get_headers(access_token, content_type_json=True) for finding in findings: status_color = self.get_color_from_status(finding.status.value) @@ -1795,16 +1807,7 @@ class Jira: message="The issue type is invalid", file=os.path.basename(__file__) ) - if self._using_basic_auth: - headers = { - "Authorization": f"Basic {access_token}", - "Content-Type": "application/json", - } - else: - headers = { - "Authorization": f"Bearer {access_token}", - "Content-Type": "application/json", - } + headers = self.get_headers(access_token, content_type_json=True) status_color = self.get_color_from_status(status) severity_color = self.get_severity_color(severity.lower()) diff --git a/tests/lib/outputs/jira/jira_test.py b/tests/lib/outputs/jira/jira_test.py index 9747bfe50f..452b4a2a51 100644 --- a/tests/lib/outputs/jira/jira_test.py +++ b/tests/lib/outputs/jira/jira_test.py @@ -721,6 +721,8 @@ class TestJiraIntegration: expected_headers = { "Authorization": "Bearer valid_access_token", "Content-Type": "application/json", + "X-Force-Accept-Language": "true", + "Accept-Language": "en", } assert call_args[0][0] == expected_url @@ -1418,3 +1420,103 @@ class TestJiraIntegration: assert result is False mock_post.assert_called_once() + + def test_get_headers_oauth_with_access_token(self): + """Test get_headers returns correct OAuth headers with access token.""" + self.jira_integration._using_basic_auth = False + + headers = self.jira_integration.get_headers( + access_token="test_oauth_token", content_type_json=True + ) + + expected_headers = { + "Authorization": "Bearer test_oauth_token", + "Content-Type": "application/json", + "X-Force-Accept-Language": "true", + "Accept-Language": "en", + } + assert headers == expected_headers + + def test_get_headers_oauth_without_content_type(self): + """Test get_headers returns OAuth headers without Content-Type when content_type_json=False.""" + self.jira_integration._using_basic_auth = False + + headers = self.jira_integration.get_headers( + access_token="test_oauth_token", content_type_json=False + ) + + expected_headers = { + "Authorization": "Bearer test_oauth_token", + "X-Force-Accept-Language": "true", + "Accept-Language": "en", + } + assert headers == expected_headers + assert "Content-Type" not in headers + + def test_get_headers_basic_auth_with_access_token(self): + """Test get_headers returns correct Basic Auth headers with access token.""" + self.jira_integration_basic_auth._using_basic_auth = True + + headers = self.jira_integration_basic_auth.get_headers( + access_token="test_basic_token", content_type_json=True + ) + + expected_headers = { + "Authorization": "Basic test_basic_token", + "Content-Type": "application/json", + "X-Force-Accept-Language": "true", + "Accept-Language": "en", + } + assert headers == expected_headers + + def test_get_headers_basic_auth_without_content_type(self): + """Test get_headers returns Basic Auth headers without Content-Type when content_type_json=False.""" + self.jira_integration_basic_auth._using_basic_auth = True + + headers = self.jira_integration_basic_auth.get_headers( + access_token="test_basic_token", content_type_json=False + ) + + expected_headers = { + "Authorization": "Basic test_basic_token", + "X-Force-Accept-Language": "true", + "Accept-Language": "en", + } + assert headers == expected_headers + assert "Content-Type" not in headers + + def test_get_headers_without_access_token_with_content_type(self): + """Test get_headers returns headers without Authorization when no access token provided.""" + headers = self.jira_integration.get_headers(content_type_json=True) + + expected_headers = { + "Content-Type": "application/json", + "X-Force-Accept-Language": "true", + "Accept-Language": "en", + } + assert headers == expected_headers + assert "Authorization" not in headers + + def test_get_headers_without_access_token_without_content_type(self): + """Test get_headers returns minimal headers when no access token and no content type.""" + headers = self.jira_integration.get_headers(content_type_json=False) + + expected_headers = { + "X-Force-Accept-Language": "true", + "Accept-Language": "en", + } + assert headers == expected_headers + assert "Authorization" not in headers + assert "Content-Type" not in headers + + def test_get_headers_default_parameters(self): + """Test get_headers with default parameters (no access token, no content type).""" + headers = self.jira_integration.get_headers() + + expected_headers = { + "X-Force-Accept-Language": "true", + "Accept-Language": "en", + } + assert headers == expected_headers + assert "Authorization" not in headers + assert "Content-Type" not in headers