Skip to content

Commit 0b7055e

Browse files
authored
feat(jira): add send_finding method with specific finding fields (#8648)
1 parent ae53b76 commit 0b7055e

File tree

3 files changed

+368
-23
lines changed

3 files changed

+368
-23
lines changed

prowler/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
1212
- `AdditionalUrls` field in CheckMetadata [(#8590)](https://github.com/prowler-cloud/prowler/pull/8590)
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)
15+
- Send finding in Jira integration with the needed values [(#8648)](https://github.com/prowler-cloud/prowler/pull/8648)
1516

1617
### Changed
1718

prowler/lib/outputs/jira/jira.py

Lines changed: 242 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -818,29 +818,30 @@ def get_severity_color(severity: str) -> str:
818818

819819
@staticmethod
820820
def get_adf_description(
821-
check_id: str = None,
822-
check_title: str = None,
823-
severity: str = None,
824-
severity_color: str = None,
825-
status: str = None,
826-
status_color: str = None,
827-
status_extended: str = None,
828-
provider: str = None,
829-
region: str = None,
830-
resource_uid: str = None,
831-
resource_name: str = None,
832-
risk: str = None,
833-
recommendation_text: str = None,
834-
recommendation_url: str = None,
835-
remediation_code_native_iac: str = None,
836-
remediation_code_terraform: str = None,
837-
remediation_code_cli: str = None,
838-
remediation_code_other: str = None,
839-
resource_tags: dict = None,
840-
compliance: dict = None,
841-
finding_url: str = None,
842-
tenant_info: str = None,
821+
check_id: str = "",
822+
check_title: str = "",
823+
severity: str = "",
824+
severity_color: str = "",
825+
status: str = "",
826+
status_color: str = "",
827+
status_extended: str = "",
828+
provider: str = "",
829+
region: str = "",
830+
resource_uid: str = "",
831+
resource_name: str = "",
832+
risk: str = "",
833+
recommendation_text: str = "",
834+
recommendation_url: str = "",
835+
remediation_code_native_iac: str = "",
836+
remediation_code_terraform: str = "",
837+
remediation_code_cli: str = "",
838+
remediation_code_other: str = "",
839+
resource_tags: dict = "",
840+
compliance: dict = "",
841+
finding_url: str = "",
842+
tenant_info: str = "",
843843
) -> dict:
844+
844845
table_rows = [
845846
{
846847
"type": "tableRow",
@@ -1618,10 +1619,21 @@ def send_findings(
16181619
finding_url=finding_url,
16191620
tenant_info=tenant_info,
16201621
)
1622+
summary_parts = ["[Prowler]"]
1623+
if finding.metadata.Severity.value:
1624+
summary_parts.append(finding.metadata.Severity.value.upper())
1625+
if finding.metadata.CheckID:
1626+
summary_parts.append(finding.metadata.CheckID)
1627+
if finding.resource_uid:
1628+
summary_parts.append(finding.resource_uid)
1629+
1630+
summary = " - ".join(summary_parts[1:])
1631+
summary = f"{summary_parts[0]} {summary}"
1632+
16211633
payload = {
16221634
"fields": {
16231635
"project": {"key": project_key},
1624-
"summary": f"[Prowler] {finding.metadata.Severity.value.upper()} - {finding.metadata.CheckID} - {finding.resource_uid}",
1636+
"summary": summary,
16251637
"description": adf_description,
16261638
"issuetype": {"name": issue_type},
16271639
}
@@ -1691,3 +1703,210 @@ def send_findings(
16911703
message="Failed to create an issue in Jira",
16921704
file=os.path.basename(__file__),
16931705
)
1706+
1707+
def send_finding(
1708+
self,
1709+
check_id: str = "",
1710+
check_title: str = "",
1711+
severity: str = "",
1712+
status: str = "",
1713+
status_extended: str = "",
1714+
provider: str = "",
1715+
region: str = "",
1716+
resource_uid: str = "",
1717+
resource_name: str = "",
1718+
risk: str = "",
1719+
recommendation_text: str = "",
1720+
recommendation_url: str = "",
1721+
remediation_code_native_iac: str = "",
1722+
remediation_code_terraform: str = "",
1723+
remediation_code_cli: str = "",
1724+
remediation_code_other: str = "",
1725+
resource_tags: dict = "",
1726+
compliance: dict = "",
1727+
project_key: str = "",
1728+
issue_type: str = "",
1729+
issue_labels: list[str] = "",
1730+
finding_url: str = "",
1731+
tenant_info: str = "",
1732+
) -> bool:
1733+
"""
1734+
Send the finding to Jira
1735+
1736+
Args:
1737+
- check_id: The check ID
1738+
- check_title: The check title
1739+
- severity: The severity
1740+
- status: The status
1741+
- status_extended: The status extended
1742+
- provider: The provider
1743+
- region: The region
1744+
- resource_uid: The resource UID
1745+
- resource_name: The resource name
1746+
- risk: The risk
1747+
- recommendation_text: The recommendation text
1748+
- recommendation_url: The recommendation URL
1749+
- remediation_code_native_iac: The remediation code native IAC
1750+
- remediation_code_terraform: The remediation code terraform
1751+
- remediation_code_cli: The remediation code CLI
1752+
- remediation_code_other: The remediation code other
1753+
- resource_tags: The resource tags
1754+
- compliance: The compliance
1755+
- project_key: The project key
1756+
- issue_type: The issue type
1757+
- issue_labels: The issue labels
1758+
- finding_url: The finding URL
1759+
- tenant_info: The tenant info
1760+
1761+
Raises:
1762+
- JiraRefreshTokenError: Failed to refresh the access token
1763+
- JiraRefreshTokenResponseError: Failed to refresh the access token, response code did not match 200
1764+
- JiraCreateIssueError: Failed to create an issue in Jira
1765+
- JiraSendFindingsResponseError: Failed to send the finding to Jira
1766+
- JiraRequiredCustomFieldsError: Jira project requires custom fields that are not supported
1767+
1768+
Returns:
1769+
- True if the finding was sent successfully
1770+
- False if the finding was not sent successfully
1771+
"""
1772+
try:
1773+
access_token = self.get_access_token()
1774+
1775+
if not access_token:
1776+
raise JiraNoTokenError(
1777+
message="No token was found",
1778+
file=os.path.basename(__file__),
1779+
)
1780+
1781+
projects = self.get_projects()
1782+
1783+
if project_key not in projects:
1784+
logger.error("The project key is invalid")
1785+
raise JiraInvalidProjectKeyError(
1786+
message="The project key is invalid",
1787+
file=os.path.basename(__file__),
1788+
)
1789+
1790+
available_issue_types = self.get_available_issue_types(project_key)
1791+
1792+
if issue_type not in available_issue_types:
1793+
logger.error("The issue type is invalid")
1794+
raise JiraInvalidIssueTypeError(
1795+
message="The issue type is invalid", file=os.path.basename(__file__)
1796+
)
1797+
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+
}
1808+
1809+
status_color = self.get_color_from_status(status)
1810+
severity_color = self.get_severity_color(severity.lower())
1811+
adf_description = self.get_adf_description(
1812+
check_id=check_id,
1813+
check_title=check_title,
1814+
severity=severity.upper(),
1815+
severity_color=severity_color,
1816+
status=status,
1817+
status_color=status_color,
1818+
status_extended=status_extended,
1819+
provider=provider,
1820+
region=region,
1821+
resource_uid=resource_uid,
1822+
resource_name=resource_name,
1823+
risk=risk,
1824+
recommendation_text=recommendation_text,
1825+
recommendation_url=recommendation_url,
1826+
remediation_code_native_iac=remediation_code_native_iac,
1827+
remediation_code_terraform=remediation_code_terraform,
1828+
remediation_code_cli=remediation_code_cli,
1829+
remediation_code_other=remediation_code_other,
1830+
resource_tags=resource_tags,
1831+
compliance=compliance,
1832+
finding_url=finding_url,
1833+
tenant_info=tenant_info,
1834+
)
1835+
1836+
summary_parts = ["[Prowler]"]
1837+
if severity:
1838+
summary_parts.append(severity.upper())
1839+
if check_id:
1840+
summary_parts.append(check_id)
1841+
if resource_uid:
1842+
summary_parts.append(resource_uid)
1843+
summary = " - ".join(summary_parts[1:])
1844+
summary = f"{summary_parts[0]} {summary}"
1845+
1846+
payload = {
1847+
"fields": {
1848+
"project": {"key": project_key},
1849+
"summary": summary,
1850+
"description": adf_description,
1851+
"issuetype": {"name": issue_type},
1852+
}
1853+
}
1854+
if issue_labels:
1855+
payload["fields"]["labels"] = issue_labels
1856+
1857+
response = requests.post(
1858+
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/issue",
1859+
json=payload,
1860+
headers=headers,
1861+
)
1862+
1863+
if response.status_code != 201:
1864+
try:
1865+
response_json = response.json()
1866+
except (ValueError, requests.exceptions.JSONDecodeError):
1867+
response_error = f"Failed to send finding: {response.status_code} - {response.text}"
1868+
logger.error(response_error)
1869+
return False
1870+
1871+
# Check if the error is due to required custom fields
1872+
if response.status_code == 400 and "errors" in response_json:
1873+
errors = response_json.get("errors", {})
1874+
# Look for custom field errors (fields starting with "customfield_")
1875+
custom_field_errors = {
1876+
k: v for k, v in errors.items() if k.startswith("customfield_")
1877+
}
1878+
if custom_field_errors:
1879+
custom_fields_formatted = ", ".join(
1880+
[f"'{k}': '{v}'" for k, v in custom_field_errors.items()]
1881+
)
1882+
logger.error(
1883+
f"Jira project requires custom fields that are not supported: {custom_fields_formatted}"
1884+
)
1885+
return False
1886+
1887+
response_error = (
1888+
f"Failed to send finding: {response.status_code} - {response_json}"
1889+
)
1890+
logger.error(response_error)
1891+
return False
1892+
else:
1893+
try:
1894+
response_json = response.json()
1895+
logger.info(f"Finding sent successfully: {response_json}")
1896+
except (ValueError, requests.exceptions.JSONDecodeError):
1897+
logger.info(
1898+
f"Finding sent successfully: Status {response.status_code}"
1899+
)
1900+
return True
1901+
except JiraRequiredCustomFieldsError as custom_fields_error:
1902+
logger.error(f"Custom fields error: {custom_fields_error}")
1903+
return False
1904+
except JiraRefreshTokenError as refresh_error:
1905+
logger.error(f"Token refresh error: {refresh_error}")
1906+
return False
1907+
except JiraRefreshTokenResponseError as response_error:
1908+
logger.error(f"Token response error: {response_error}")
1909+
return False
1910+
except Exception as e:
1911+
logger.error(f"Failed to send finding: {e}")
1912+
return False

0 commit comments

Comments
 (0)