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