feat(jira): request timeout (#11602)

This commit is contained in:
Pepe Fagoaga
2026-06-16 09:36:22 +02:00
committed by GitHub
parent 810d8d7686
commit ca7ce5a8c3
3 changed files with 108 additions and 3 deletions
+1
View File
@@ -14,6 +14,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `identity_storage_service_level_admins_scoped` check for OCI provider CIS 3.1 control 1.15, ensuring storage service-level administrators exclude delete permissions [(#11523)](https://github.com/prowler-cloud/prowler/pull/11523)
- `cosmosdb_account_automatic_failover_enabled` check for Azure provider [(#11031)](https://github.com/prowler-cloud/prowler/pull/11031)
- `cosmosdb_account_backup_policy_continuous` check for Azure provider [(#11032)](https://github.com/prowler-cloud/prowler/pull/11032)
- Jira timeout preventing the calls from hanging indefinitely when the Jira endpoint is unreachable or slow [(#11602)](https://github.com/prowler-cloud/prowler/pull/11602)
- TLS certificate verification in the `codepipeline_project_repo_private` check, which previously used an unverified SSL context, leaving the repository-visibility probe open to MITM tampering [(#11603)](https://github.com/prowler-cloud/prowler/pull/11603)
### 🔄 Changed
+25 -3
View File
@@ -341,6 +341,7 @@ class Jira:
}
TOKEN_URL = "https://auth.atlassian.com/oauth/token"
API_TOKEN_URL = "https://api.atlassian.com/oauth/token/accessible-resources"
REQUEST_TIMEOUT = 30
HEADER_TEMPLATE = {
"Content-Type": "application/json",
"X-Force-Accept-Language": "true",
@@ -578,7 +579,12 @@ class Jira:
}
headers = self.get_headers(content_type_json=True)
response = requests.post(self.TOKEN_URL, json=body, headers=headers)
response = requests.post(
self.TOKEN_URL,
json=body,
headers=headers,
timeout=self.REQUEST_TIMEOUT,
)
if response.status_code == 200:
tokens = response.json()
@@ -630,12 +636,17 @@ class Jira:
response = requests.get(
f"https://{domain}.atlassian.net/_edge/tenant_info",
headers=headers,
timeout=self.REQUEST_TIMEOUT,
)
response = response.json()
return response.get("cloudId")
else:
headers = self.get_headers(access_token)
response = requests.get(self.API_TOKEN_URL, headers=headers)
response = requests.get(
self.API_TOKEN_URL,
headers=headers,
timeout=self.REQUEST_TIMEOUT,
)
if response.status_code == 200:
resources = response.json()
@@ -717,7 +728,12 @@ class Jira:
}
headers = self.get_headers(content_type_json=True)
response = requests.post(url, json=body, headers=headers)
response = requests.post(
url,
json=body,
headers=headers,
timeout=self.REQUEST_TIMEOUT,
)
if response.status_code == 200:
tokens = response.json()
@@ -874,6 +890,7 @@ class Jira:
response = requests.get(
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/project",
headers=headers,
timeout=self.REQUEST_TIMEOUT,
)
if response.status_code == 200:
@@ -941,6 +958,7 @@ class Jira:
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",
headers=headers,
timeout=self.REQUEST_TIMEOUT,
)
if response.status_code == 200:
@@ -986,6 +1004,7 @@ class Jira:
response = requests.get(
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/project",
headers=headers,
timeout=self.REQUEST_TIMEOUT,
)
if response.status_code == 200:
projects_data = {}
@@ -1001,6 +1020,7 @@ class Jira:
project_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",
headers=headers,
timeout=self.REQUEST_TIMEOUT,
)
if project_response.status_code == 200:
project_metadata = project_response.json()
@@ -1923,6 +1943,7 @@ class Jira:
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/issue",
json=payload,
headers=headers,
timeout=self.REQUEST_TIMEOUT,
)
if response.status_code != 201:
@@ -2127,6 +2148,7 @@ class Jira:
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/issue",
json=payload,
headers=headers,
timeout=self.REQUEST_TIMEOUT,
)
if response.status_code != 201:
+82
View File
@@ -339,6 +339,88 @@ class TestJiraIntegration:
with pytest.raises(JiraRefreshTokenError):
self.jira_integration.refresh_access_token()
@patch("prowler.lib.outputs.jira.jira.requests.post")
@patch.object(Jira, "get_cloud_id", return_value="test_cloud_id")
def test_get_auth_sends_timeout(self, mock_get_cloud_id, mock_post):
"""get_auth must pass a request timeout to avoid hanging on an unresponsive Jira."""
# To disable vulture
mock_get_cloud_id = mock_get_cloud_id
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"access_token": "test_access_token",
"refresh_token": "test_refresh_token",
"expires_in": 3600,
}
mock_post.return_value = mock_response
self.jira_integration.get_auth("test_auth_code")
assert mock_post.call_args.kwargs["timeout"] == Jira.REQUEST_TIMEOUT
@patch("prowler.lib.outputs.jira.jira.requests.get")
def test_get_cloud_id_sends_timeout(self, mock_get):
"""get_cloud_id (OAuth path) must pass a request timeout."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = [{"id": "test_cloud_id"}]
mock_get.return_value = mock_response
self.jira_integration.get_cloud_id("test_access_token")
assert mock_get.call_args.kwargs["timeout"] == Jira.REQUEST_TIMEOUT
@patch("prowler.lib.outputs.jira.jira.requests.get")
def test_get_cloud_id_basic_auth_sends_timeout(self, mock_get):
"""get_cloud_id (basic-auth tenant_info path) must pass a request timeout."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"cloudId": "test_cloud_id"}
mock_get.return_value = mock_response
self.jira_integration_basic_auth.get_cloud_id(domain=self.domain)
assert mock_get.call_args.kwargs["timeout"] == Jira.REQUEST_TIMEOUT
@patch("prowler.lib.outputs.jira.jira.requests.post")
def test_refresh_access_token_sends_timeout(self, mock_post):
"""refresh_access_token must pass a request timeout."""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"access_token": "new_access_token",
"refresh_token": "new_refresh_token",
"expires_in": 3600,
}
mock_post.return_value = mock_response
self.jira_integration.refresh_access_token()
assert mock_post.call_args.kwargs["timeout"] == Jira.REQUEST_TIMEOUT
@patch.object(Jira, "get_access_token", return_value="valid_access_token")
@patch.object(
Jira, "cloud_id", new_callable=PropertyMock, return_value="test_cloud_id"
)
@patch("prowler.lib.outputs.jira.jira.requests.get")
def test_get_projects_sends_timeout(
self, mock_get, mock_cloud_id, mock_get_access_token
):
"""get_projects must pass a request timeout."""
# To disable vulture
mock_cloud_id = mock_cloud_id
mock_get_access_token = mock_get_access_token
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = [{"key": "PROJ1", "name": "Project One"}]
mock_get.return_value = mock_response
self.jira_integration.get_projects()
assert mock_get.call_args.kwargs["timeout"] == Jira.REQUEST_TIMEOUT
@patch.object(Jira, "get_auth", return_value=None)
@patch.object(
Jira,