Compare commits

...

12 Commits

Author SHA1 Message Date
pedrooot
5d623cf831 feat(jira): change logger mode 2025-09-05 09:08:02 +02:00
pedrooot
01fa679f9e feat(jira): update with master 2025-09-04 09:15:30 +02:00
pedrooot
2d1cba2972 feat(jira): merge master 2025-09-04 09:11:06 +02:00
Pedro Martín
3784a57722 Merge branch 'master' into PRWLR-7874-support-labels-on-jira-tickets 2025-09-03 13:55:15 +02:00
Pedro Martín
f96a3ac0ea Merge branch 'master' into PRWLR-7874-support-labels-on-jira-tickets 2025-09-03 10:49:00 +02:00
Pedro Martín
f08850daee feat(jira): add ui link and tenant info to ticket (#8607) 2025-09-03 10:46:09 +02:00
Pedro Martín
5c2ee5463b Merge branch 'master' into PRWLR-7874-support-labels-on-jira-tickets 2025-09-02 11:51:36 +02:00
pedrooot
d2b7d6c187 feat(changelog): update with latests changes 2025-08-28 15:40:01 +02:00
pedrooot
199efd290d feat(jira): support issue labels 2025-08-28 15:34:15 +02:00
pedrooot
08660026d2 feat(changelog): update with latests changes 2025-08-28 15:32:38 +02:00
pedrooot
f8c0ff074b feat(changelog): update with latests changes 2025-08-28 14:55:19 +02:00
pedrooot
cb7941e9be feat(jira): add data to table and error handling 2025-08-28 14:10:26 +02:00
3 changed files with 152 additions and 11 deletions

View File

@@ -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)

View File

@@ -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:

View File

@@ -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(