mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-04-14 08:28:16 +00:00
Compare commits
12 Commits
feat/prowl
...
PRWLR-7873
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d623cf831 | ||
|
|
01fa679f9e | ||
|
|
2d1cba2972 | ||
|
|
3784a57722 | ||
|
|
f96a3ac0ea | ||
|
|
f08850daee | ||
|
|
5c2ee5463b | ||
|
|
d2b7d6c187 | ||
|
|
199efd290d | ||
|
|
08660026d2 | ||
|
|
f8c0ff074b | ||
|
|
cb7941e9be |
@@ -4,8 +4,10 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
|||||||
## [v5.12.0] (Prowler UNRELEASED)
|
## [v5.12.0] (Prowler UNRELEASED)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Get Jira Project's metadata [(#8630)](https://github.com/prowler-cloud/prowler/pull/8630)
|
|
||||||
- Add more fields for the Jira ticket and handle custom fields errors [(#8601)](https://github.com/prowler-cloud/prowler/pull/8601)
|
- Add more fields for the Jira ticket and handle custom fields errors [(#8601)](https://github.com/prowler-cloud/prowler/pull/8601)
|
||||||
|
- Support labels on Jira tickets [(#8603)](https://github.com/prowler-cloud/prowler/pull/8603)
|
||||||
|
- Add finding url and tenant info inside Jira tickets [(#8607)](https://github.com/prowler-cloud/prowler/pull/8607)
|
||||||
|
- Get Jira Project's metadata [(#8630)](https://github.com/prowler-cloud/prowler/pull/8630)
|
||||||
- Get Jira projects from test_connection [(#8634)](https://github.com/prowler-cloud/prowler/pull/8634)
|
- Get Jira projects from test_connection [(#8634)](https://github.com/prowler-cloud/prowler/pull/8634)
|
||||||
- `AdditionalUrls` field in CheckMetadata [(#8590)](https://github.com/prowler-cloud/prowler/pull/8590)
|
- `AdditionalUrls` field in CheckMetadata [(#8590)](https://github.com/prowler-cloud/prowler/pull/8590)
|
||||||
- Support color for MANUAL finidngs in Jira tickets [(#8642)](https://github.com/prowler-cloud/prowler/pull/8642)
|
- Support color for MANUAL finidngs in Jira tickets [(#8642)](https://github.com/prowler-cloud/prowler/pull/8642)
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ class Jira:
|
|||||||
- test_connection: Test the connection to Jira and return a Connection object
|
- test_connection: Test the connection to Jira and return a Connection object
|
||||||
- get_projects: Get the projects from Jira
|
- get_projects: Get the projects from Jira
|
||||||
- get_available_issue_types: Get the available issue types for a project
|
- get_available_issue_types: Get the available issue types for a project
|
||||||
|
- get_available_issue_labels: Get the available labels for a project
|
||||||
- send_findings: Send the findings to Jira and create an issue
|
- send_findings: Send the findings to Jira and create an issue
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
@@ -377,7 +378,7 @@ class Jira:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
response_error = f"Failed to get cloud id: {response.status_code} - {response.json()}"
|
response_error = f"Failed to get cloud id: {response.status_code} - {response.json()}"
|
||||||
logger.warning(response_error)
|
logger.error(response_error)
|
||||||
raise JiraGetCloudIDResponseError(
|
raise JiraGetCloudIDResponseError(
|
||||||
message=response_error, file=os.path.basename(__file__)
|
message=response_error, file=os.path.basename(__file__)
|
||||||
)
|
)
|
||||||
@@ -454,7 +455,7 @@ class Jira:
|
|||||||
return self._access_token
|
return self._access_token
|
||||||
else:
|
else:
|
||||||
response_error = f"Failed to refresh access token: {response.status_code} - {response.json()}"
|
response_error = f"Failed to refresh access token: {response.status_code} - {response.json()}"
|
||||||
logger.warning(response_error)
|
logger.error(response_error)
|
||||||
raise JiraRefreshTokenResponseError(
|
raise JiraRefreshTokenResponseError(
|
||||||
message=response_error, file=os.path.basename(__file__)
|
message=response_error, file=os.path.basename(__file__)
|
||||||
)
|
)
|
||||||
@@ -672,7 +673,7 @@ class Jira:
|
|||||||
return [issue_type["name"] for issue_type in issue_types]
|
return [issue_type["name"] for issue_type in issue_types]
|
||||||
else:
|
else:
|
||||||
response_error = f"Failed to get available issue types: {response.status_code} - {response.text}"
|
response_error = f"Failed to get available issue types: {response.status_code} - {response.text}"
|
||||||
logger.warning(response_error)
|
logger.error(response_error)
|
||||||
raise JiraGetAvailableIssueTypesResponseError(
|
raise JiraGetAvailableIssueTypesResponseError(
|
||||||
message=response_error, file=os.path.basename(__file__)
|
message=response_error, file=os.path.basename(__file__)
|
||||||
)
|
)
|
||||||
@@ -726,7 +727,7 @@ class Jira:
|
|||||||
if project_response.status_code == 200:
|
if project_response.status_code == 200:
|
||||||
project_metadata = project_response.json()
|
project_metadata = project_response.json()
|
||||||
if len(project_metadata["projects"]) == 0:
|
if len(project_metadata["projects"]) == 0:
|
||||||
logger.warning(
|
logger.error(
|
||||||
f"No project metadata found for project {project['key']}, setting empty issue types"
|
f"No project metadata found for project {project['key']}, setting empty issue types"
|
||||||
)
|
)
|
||||||
issue_types = []
|
issue_types = []
|
||||||
@@ -837,6 +838,8 @@ class Jira:
|
|||||||
remediation_code_other: str = None,
|
remediation_code_other: str = None,
|
||||||
resource_tags: dict = None,
|
resource_tags: dict = None,
|
||||||
compliance: dict = None,
|
compliance: dict = None,
|
||||||
|
finding_url: str = None,
|
||||||
|
tenant_info: str = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
table_rows = [
|
table_rows = [
|
||||||
{
|
{
|
||||||
@@ -1414,6 +1417,93 @@ class Jira:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if finding_url:
|
||||||
|
table_rows.append(
|
||||||
|
{
|
||||||
|
"type": "tableRow",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "tableCell",
|
||||||
|
"attrs": {"colwidth": [1]},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "Finding URL",
|
||||||
|
"marks": [{"type": "strong"}],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tableCell",
|
||||||
|
"attrs": {"colwidth": [3]},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": finding_url,
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"type": "link",
|
||||||
|
"attrs": {"href": finding_url},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if tenant_info:
|
||||||
|
table_rows.append(
|
||||||
|
{
|
||||||
|
"type": "tableRow",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "tableCell",
|
||||||
|
"attrs": {"colwidth": [1]},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "Tenant Info",
|
||||||
|
"marks": [{"type": "strong"}],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tableCell",
|
||||||
|
"attrs": {"colwidth": [3]},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": tenant_info,
|
||||||
|
"marks": [{"type": "code"}],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"type": "doc",
|
"type": "doc",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
@@ -1440,6 +1530,9 @@ class Jira:
|
|||||||
findings: list[Finding] = None,
|
findings: list[Finding] = None,
|
||||||
project_key: str = None,
|
project_key: str = None,
|
||||||
issue_type: str = None,
|
issue_type: str = None,
|
||||||
|
issue_labels: list[str] = None,
|
||||||
|
finding_url: str = None,
|
||||||
|
tenant_info: str = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Send the findings to Jira
|
Send the findings to Jira
|
||||||
@@ -1448,6 +1541,9 @@ class Jira:
|
|||||||
- findings: The findings to send
|
- findings: The findings to send
|
||||||
- project_key: The project key
|
- project_key: The project key
|
||||||
- issue_type: The issue type
|
- issue_type: The issue type
|
||||||
|
- issue_labels: The issue labels
|
||||||
|
- finding_url: The finding URL
|
||||||
|
- tenant_info: The tenant info
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
- JiraRefreshTokenError: Failed to refresh the access token
|
- JiraRefreshTokenError: Failed to refresh the access token
|
||||||
@@ -1519,6 +1615,8 @@ class Jira:
|
|||||||
remediation_code_other=finding.metadata.Remediation.Code.Other,
|
remediation_code_other=finding.metadata.Remediation.Code.Other,
|
||||||
resource_tags=finding.resource_tags,
|
resource_tags=finding.resource_tags,
|
||||||
compliance=finding.compliance,
|
compliance=finding.compliance,
|
||||||
|
finding_url=finding_url,
|
||||||
|
tenant_info=tenant_info,
|
||||||
)
|
)
|
||||||
payload = {
|
payload = {
|
||||||
"fields": {
|
"fields": {
|
||||||
@@ -1528,6 +1626,8 @@ class Jira:
|
|||||||
"issuetype": {"name": issue_type},
|
"issuetype": {"name": issue_type},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if issue_labels:
|
||||||
|
payload["fields"]["labels"] = issue_labels
|
||||||
|
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/issue",
|
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/issue",
|
||||||
@@ -1536,7 +1636,15 @@ class Jira:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code != 201:
|
if response.status_code != 201:
|
||||||
response_json = response.json()
|
try:
|
||||||
|
response_json = response.json()
|
||||||
|
except (ValueError, requests.exceptions.JSONDecodeError):
|
||||||
|
response_error = f"Failed to send finding: {response.status_code} - {response.text}"
|
||||||
|
logger.error(response_error)
|
||||||
|
raise JiraSendFindingsResponseError(
|
||||||
|
message=response_error, file=os.path.basename(__file__)
|
||||||
|
)
|
||||||
|
|
||||||
# Check if the error is due to required custom fields
|
# Check if the error is due to required custom fields
|
||||||
if response.status_code == 400 and "errors" in response_json:
|
if response.status_code == 400 and "errors" in response_json:
|
||||||
errors = response_json.get("errors", {})
|
errors = response_json.get("errors", {})
|
||||||
@@ -1559,12 +1667,18 @@ class Jira:
|
|||||||
)
|
)
|
||||||
|
|
||||||
response_error = f"Failed to send finding: {response.status_code} - {response_json}"
|
response_error = f"Failed to send finding: {response.status_code} - {response_json}"
|
||||||
logger.warning(response_error)
|
logger.error(response_error)
|
||||||
raise JiraSendFindingsResponseError(
|
raise JiraSendFindingsResponseError(
|
||||||
message=response_error, file=os.path.basename(__file__)
|
message=response_error, file=os.path.basename(__file__)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.info(f"Finding sent successfully: {response.json()}")
|
try:
|
||||||
|
response_json = response.json()
|
||||||
|
logger.info(f"Finding sent successfully: {response_json}")
|
||||||
|
except (ValueError, requests.exceptions.JSONDecodeError):
|
||||||
|
logger.info(
|
||||||
|
f"Finding sent successfully: Status {response.status_code}"
|
||||||
|
)
|
||||||
except JiraRequiredCustomFieldsError as custom_fields_error:
|
except JiraRequiredCustomFieldsError as custom_fields_error:
|
||||||
raise custom_fields_error
|
raise custom_fields_error
|
||||||
except JiraRefreshTokenError as refresh_error:
|
except JiraRefreshTokenError as refresh_error:
|
||||||
|
|||||||
@@ -699,13 +699,22 @@ class TestJiraIntegration:
|
|||||||
self.jira_integration.cloud_id = "valid_cloud_id"
|
self.jira_integration.cloud_id = "valid_cloud_id"
|
||||||
|
|
||||||
self.jira_integration.send_findings(
|
self.jira_integration.send_findings(
|
||||||
findings=[finding], project_key="TEST-1", issue_type="Bug"
|
findings=[finding],
|
||||||
|
project_key="TEST-1",
|
||||||
|
issue_type="Bug",
|
||||||
|
issue_labels=["scan-mocked", "whatever"],
|
||||||
|
finding_url="https://prowler-cloud-link/findings/12345",
|
||||||
|
tenant_info="Tenant Info",
|
||||||
)
|
)
|
||||||
|
|
||||||
mock_post.assert_called_once()
|
mock_post.assert_called_once()
|
||||||
|
|
||||||
call_args = mock_post.call_args
|
call_args = mock_post.call_args
|
||||||
|
|
||||||
|
mock_post.assert_called_once()
|
||||||
|
|
||||||
|
call_args = mock_post.call_args
|
||||||
|
|
||||||
expected_url = (
|
expected_url = (
|
||||||
"https://api.atlassian.com/ex/jira/valid_cloud_id/rest/api/3/issue"
|
"https://api.atlassian.com/ex/jira/valid_cloud_id/rest/api/3/issue"
|
||||||
)
|
)
|
||||||
@@ -722,6 +731,7 @@ class TestJiraIntegration:
|
|||||||
assert payload["fields"]["summary"] == "[Prowler] HIGH - CHECK-1 - resource-1"
|
assert payload["fields"]["summary"] == "[Prowler] HIGH - CHECK-1 - resource-1"
|
||||||
assert payload["fields"]["issuetype"]["name"] == "Bug"
|
assert payload["fields"]["issuetype"]["name"] == "Bug"
|
||||||
assert payload["fields"]["description"]["type"] == "doc"
|
assert payload["fields"]["description"]["type"] == "doc"
|
||||||
|
assert payload["fields"]["labels"] == ["scan-mocked", "whatever"]
|
||||||
|
|
||||||
description_content = payload["fields"]["description"]["content"]
|
description_content = payload["fields"]["description"]["content"]
|
||||||
|
|
||||||
@@ -772,6 +782,8 @@ class TestJiraIntegration:
|
|||||||
"Remediation Terraform",
|
"Remediation Terraform",
|
||||||
"Remediation CLI",
|
"Remediation CLI",
|
||||||
"Remediation Other",
|
"Remediation Other",
|
||||||
|
"Finding URL",
|
||||||
|
"Tenant Info",
|
||||||
]
|
]
|
||||||
|
|
||||||
actual_keys = [key for key, _ in row_texts]
|
actual_keys = [key for key, _ in row_texts]
|
||||||
@@ -811,6 +823,8 @@ class TestJiraIntegration:
|
|||||||
assert "Owner=SecurityTeam" in row_dict["Resource Tags"]
|
assert "Owner=SecurityTeam" in row_dict["Resource Tags"]
|
||||||
assert "CIS: 2.1.1, 2.1.2" in row_dict["Compliance"]
|
assert "CIS: 2.1.1, 2.1.2" in row_dict["Compliance"]
|
||||||
assert "NIST: AC-3, AC-6" in row_dict["Compliance"]
|
assert "NIST: AC-3, AC-6" in row_dict["Compliance"]
|
||||||
|
assert "https://prowler-cloud-link/findings/12345" in row_dict["Finding URL"]
|
||||||
|
assert "Tenant Info" in row_dict["Tenant Info"]
|
||||||
|
|
||||||
@patch.object(Jira, "get_access_token", return_value="valid_access_token")
|
@patch.object(Jira, "get_access_token", return_value="valid_access_token")
|
||||||
@patch.object(
|
@patch.object(
|
||||||
@@ -825,6 +839,7 @@ class TestJiraIntegration:
|
|||||||
mock_get_available_issue_types = mock_get_available_issue_types
|
mock_get_available_issue_types = mock_get_available_issue_types
|
||||||
mock_get_access_token = mock_get_access_token
|
mock_get_access_token = mock_get_access_token
|
||||||
mock_post = mock_post
|
mock_post = mock_post
|
||||||
|
mock_post = mock_post
|
||||||
|
|
||||||
with pytest.raises(JiraCreateIssueError):
|
with pytest.raises(JiraCreateIssueError):
|
||||||
self.jira_integration.send_findings(
|
self.jira_integration.send_findings(
|
||||||
@@ -914,7 +929,12 @@ class TestJiraIntegration:
|
|||||||
|
|
||||||
with pytest.raises(JiraRequiredCustomFieldsError):
|
with pytest.raises(JiraRequiredCustomFieldsError):
|
||||||
self.jira_integration.send_findings(
|
self.jira_integration.send_findings(
|
||||||
findings=[finding], project_key="TEST-1", issue_type="Bug"
|
findings=[finding],
|
||||||
|
project_key="TEST-1",
|
||||||
|
issue_type="Bug",
|
||||||
|
issue_labels=["scan-mocked", "whatever"],
|
||||||
|
finding_url="https://prowler-cloud-link/findings/12345",
|
||||||
|
tenant_info="Tenant Info",
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch.object(Jira, "get_access_token", return_value="valid_access_token")
|
@patch.object(Jira, "get_access_token", return_value="valid_access_token")
|
||||||
@@ -971,7 +991,12 @@ class TestJiraIntegration:
|
|||||||
|
|
||||||
with pytest.raises(JiraCreateIssueError):
|
with pytest.raises(JiraCreateIssueError):
|
||||||
self.jira_integration.send_findings(
|
self.jira_integration.send_findings(
|
||||||
findings=[finding], project_key="TEST-1", issue_type="Bug"
|
findings=[finding],
|
||||||
|
project_key="TEST-1",
|
||||||
|
issue_type="Bug",
|
||||||
|
issue_labels=["scan-mocked", "whatever"],
|
||||||
|
finding_url="https://prowler-cloud-link/findings/12345",
|
||||||
|
tenant_info="Tenant Info",
|
||||||
)
|
)
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
|||||||
Reference in New Issue
Block a user