diff --git a/freshmaker/config.py b/freshmaker/config.py index 851b747f..5ad9dc42 100644 --- a/freshmaker/config.py +++ b/freshmaker/config.py @@ -467,6 +467,16 @@ class Config(object): "default": "", "desc": "URL to a remote file containing image repositories enabled for rebuilding due to compliance priority CVEs", }, + "jira_server_url": { + "type": str, + "default": "https://issues.redhat.com", + "desc": "The JIRA server url", + }, + "jira_token": { + "type": str, + "default": "", + "desc": "A string of the token necessary for JIRA PAT bearer token authorization", + }, } def __init__(self, conf_section_obj): diff --git a/freshmaker/errata.py b/freshmaker/errata.py index c423ee41..ff20391c 100644 --- a/freshmaker/errata.py +++ b/freshmaker/errata.py @@ -22,10 +22,10 @@ # Written by Jan Kaluza import os -import re import requests import dogpile.cache from requests_kerberos import HTTPKerberosAuth, OPTIONAL +from jira import JIRA from freshmaker.events import BrewSignRPMEvent, ErrataBaseEvent, FreshmakerManualRebuildEvent from freshmaker import conf, log @@ -47,8 +47,9 @@ def __init__( product_short_name=None, release_name=None, cve_list=None, - is_major_incident=None, - is_compliance_priority=None, + is_major_incident=False, + is_compliance_priority=False, + is_contract_priority=False, ): """ Initializes the ErrataAdvisory instance. @@ -63,6 +64,7 @@ def __init__( self.cve_list = cve_list or [] self.is_major_incident = is_major_incident self.is_compliance_priority = is_compliance_priority + self.is_contract_priority = is_contract_priority self._affected_rpm_nvrs = None self._reporter = "" @@ -120,20 +122,9 @@ def from_advisory_id(cls, errata, errata_id): # rebuilds for artifacts. security_impact = erratum_data["security_impact"].lower() - has_hightouch_bug = False - has_compliance_priority_bug = False - bugs = errata._get_bugs(erratum_data["id"]) or [] - - has_hightouch_bug = any(["hightouch+" in bug.get("flags", "") for bug in bugs]) - has_compliance_priority_bug = any( - ["compliance_priority+" in bug.get("flags", "") for bug in bugs] - ) - - has_jira_major_incident = errata.has_jira_major_incidents(errata_id) - is_major_incident = has_hightouch_bug or has_jira_major_incident - - has_jira_compliance_priority = errata.has_compliance_priority_jira_label(errata_id) - is_compliance_priority = has_compliance_priority_bug or has_jira_compliance_priority + is_major_incident = errata.is_major_incident_advisory(errata_id) + is_compliance_priority = errata.is_compliance_priority_advisory(errata_id) + is_contract_priority = errata.is_contract_priority_advisory(errata_id) return ErrataAdvisory( erratum_data["id"], @@ -146,6 +137,7 @@ def from_advisory_id(cls, errata, errata_id): cve_list, is_major_incident, is_compliance_priority, + is_contract_priority, ) def is_flatpak_module_advisory_ready(self): @@ -534,56 +526,49 @@ def is_zstream(self, errata_id): release = self._get_release(errata_id) return release["data"]["attributes"]["type"] == "Zstream" - def has_jira_major_incidents(self, errata_id: str) -> bool: - """ - Checks if this errata has a 'major incident' issue in JIRA - - :param errata_id: The ID of the errata advisory - :type errata_id: str - - :return: Wether the errata has or not a major incident issue in JIRA - :rtype: bool - """ - resp = self._get_jira_issues(errata_id=errata_id) - - if isinstance(resp, dict) and resp.get("error", False): - log.info( - f"Error when querying for Jira issues for advisory {errata_id}: {resp['error']}" - ) - return False - - mi_pattern = re.compile(r"major\s*incident", re.IGNORECASE) - - for issue in resp: - if mi_pattern.search(issue["summary"]): - log.info(f"Found 'major incident' issue for advisory {errata_id}: {issue['key']}") - return True - - return False - - def has_compliance_priority_jira_label(self, errata_id) -> bool: - """ - Checks if this erratas has an issue in JIRA with 'compliance_priority' label. - - :param errata_id: The ID of the errata advisory - :type errata_id: str - - :return: Wether the errata has or not a compliance priority issue in JIRA - :rtype: bool - """ - resp = self._get_jira_issues(errata_id=errata_id) - - if isinstance(resp, dict) and resp.get("error", False): - log.info( - f"Error when querying for Jira issues for advisory {errata_id}: {resp['error']}" - ) + @retry(wait_on=Exception, logger=log) + def _check_jira_special_handling(self, issue_keys: list[str], handling_value: str) -> bool: + """Check if any vulnerability issue has the specified special handling value.""" + jira_server = JIRA(server=conf.jira_server_url, token_auth=conf.jira_token) + try: + for issue in issue_keys: + jira_issue = jira_server.issue(issue) + if jira_issue.fields.issuetype.name.lower() != "vulnerability": + continue + special_handling = getattr(jira_issue.fields, "customfield_12324753", []) or [] + if handling_value in [x.value for x in special_handling]: + return True return False - - for issue in resp: - if "compliance-priority" in issue["labels"]: - log.info( - f"Found 'compliance-priority' label in issue {issue['key']} for advisory {errata_id}" - ) - return True - - return False + finally: + jira_server.close() + + def is_major_incident_advisory(self, errata_id) -> bool: + """Check if this advisory is a major incident advisory.""" + # check if there is any "hightouch+" bug attached + bugs = self._get_bugs(errata_id) + if bugs and any(["hightouch+" in bug.get("flags", "") for bug in bugs]): + return True + + # check if there is any "Major Incident" Jira Vulnerability issue attached + issues = self._get_jira_issues(errata_id) + issue_keys = [x["key"] for x in issues] + return self._check_jira_special_handling(issue_keys, "Major Incident") + + def is_compliance_priority_advisory(self, errata_id) -> bool: + """Check if this advisory is a compliance priority advisory.""" + # check if there is any "compliance_priority+" bug attached + bugs = self._get_bugs(errata_id) + if bugs and any(["compliance_priority+" in bug.get("flags", "") for bug in bugs]): + return True + + # check if there is any "compliance-priority" Jira Vulnerability issue attached + issues = self._get_jira_issues(errata_id) + issue_keys = [x["key"] for x in issues] + return self._check_jira_special_handling(issue_keys, "compliance-priority") + + def is_contract_priority_advisory(self, errata_id) -> bool: + """Check if this advisory is a contract priority advisory.""" + # check if there is any "contract-priority" Jira Vulnerability issue attached + issues = self._get_jira_issues(errata_id) + issue_keys = [x["key"] for x in issues] + return self._check_jira_special_handling(issue_keys, "contract-priority") diff --git a/requirements.in b/requirements.in index 8c90bdcf..4b7e4086 100644 --- a/requirements.in +++ b/requirements.in @@ -10,6 +10,7 @@ Flask-Migrate Flask-SQLAlchemy gql[requests, aiohttp] httplib2 +jira jsonformatter kobo koji diff --git a/requirements.txt b/requirements.txt index a5440cf3..2f368e41 100644 --- a/requirements.txt +++ b/requirements.txt @@ -50,7 +50,9 @@ decorator==5.1.1 # gssapi # moksha-common defusedxml==0.7.1 - # via -r requirements.in + # via + # -r requirements.in + # jira dogpile-cache==1.2.2 # via -r requirements.in fedmsg==1.1.7 @@ -98,6 +100,8 @@ itsdangerous==2.1.2 # via flask jinja2==3.1.2 # via flask +jira==3.8.0 + # via -r requirements.in jsonformatter==0.3.2 # via -r requirements.in kitchen==1.2.6 @@ -136,14 +140,20 @@ munch==4.0.0 # python-fedora mypy-extensions==1.0.0 # via -r requirements.in +oauthlib==3.2.2 + # via requests-oauthlib odcs[client]==0.7.0 # via -r requirements.in openidc-client==0.6.0 # via # odcs # python-fedora +packaging==24.2 + # via jira pbr==6.0.0 # via stevedore +pillow==11.1.0 + # via jira prometheus-client==0.19.0 # via -r requirements.in psutil==5.9.6 @@ -196,12 +206,14 @@ requests==2.31.0 # -r requirements.in # fedmsg # gql + # jira # koji # odcs # openidc-client # python-fedora # requests-gssapi # requests-kerberos + # requests-oauthlib # requests-toolbelt requests-gssapi==1.2.3 # via @@ -209,8 +221,12 @@ requests-gssapi==1.2.3 # odcs requests-kerberos==0.14.0 # via -r requirements.in +requests-oauthlib==2.0.0 + # via jira requests-toolbelt==1.0.0 - # via gql + # via + # gql + # jira rpm==0.1.0 # via -r requirements.in semver==2.13.0 @@ -248,6 +264,7 @@ types-python-dateutil==2.8.19.14 typing-extensions==4.9.0 # via # alembic + # jira # twisted urllib3==2.1.0 # via diff --git a/tests/test_errata.py b/tests/test_errata.py index eddafd74..aae896c9 100644 --- a/tests/test_errata.py +++ b/tests/test_errata.py @@ -307,9 +307,10 @@ def setUp(self): def tearDown(self): super(TestErrata, self).tearDown() + @patch("freshmaker.errata.JIRA") @patch.object(Errata, "_errata_rest_get") @patch.object(Errata, "_errata_http_get") - def test_advisories_from_event(self, errata_http_get, errata_rest_get): + def test_advisories_from_event_x(self, errata_http_get, errata_rest_get, mocked_jira): MockedErrataAPI(errata_rest_get, errata_http_get) event = BrewSignRPMEvent("msgid", "libntirpc-1.4.3-4.el7rhgs") advisories = self.errata.advisories_from_event(event) @@ -323,9 +324,10 @@ def test_advisories_from_event(self, errata_http_get, errata_rest_get): self.assertEqual(advisories[0].cve_list, ["CVE-2015-3253", "CVE-2016-6814"]) self.assertEqual(advisories[0].is_major_incident, True) + @patch("freshmaker.errata.JIRA") @patch.object(Errata, "_errata_rest_get") @patch.object(Errata, "_errata_http_get") - def test_advisories_from_event_empty_cve(self, errata_http_get, errata_rest_get): + def test_advisories_from_event_empty_cve(self, errata_http_get, errata_rest_get, mocked_jira): mocked_errata = MockedErrataAPI(errata_rest_get, errata_http_get) mocked_errata.advisory_rest_json["content"]["content"]["cve"] = "" event = BrewSignRPMEvent("msgid", "libntirpc-1.4.3-4.el7rhgs") @@ -333,9 +335,11 @@ def test_advisories_from_event_empty_cve(self, errata_http_get, errata_rest_get) self.assertEqual(len(advisories), 1) self.assertEqual(advisories[0].cve_list, []) + @patch("freshmaker.errata.JIRA") @patch.object(Errata, "_errata_rest_get") @patch.object(Errata, "_errata_http_get") - def test_advisories_from_event_no_bugs(self, errata_http_get, errata_rest_get): + def test_advisories_from_event_no_bugs(self, errata_http_get, errata_rest_get, mocked_jira): + mocked_jira.return_value.issue.return_value.fields.issuetype.name = "something" mocked_errata = MockedErrataAPI(errata_rest_get, errata_http_get) mocked_errata.bugs = [] event = BrewSignRPMEvent("msgid", "libntirpc-1.4.3-4.el7rhgs") @@ -343,9 +347,12 @@ def test_advisories_from_event_no_bugs(self, errata_http_get, errata_rest_get): self.assertEqual(len(advisories), 1) self.assertEqual(advisories[0].is_major_incident, False) + @patch("freshmaker.errata.JIRA") @patch.object(Errata, "_errata_rest_get") @patch.object(Errata, "_errata_http_get") - def test_advisories_from_event_empty_bug_flags(self, errata_http_get, errata_rest_get): + def test_advisories_from_event_empty_bug_flags( + self, errata_http_get, errata_rest_get, mocked_jira + ): mocked_errata = MockedErrataAPI(errata_rest_get, errata_http_get) for bug in mocked_errata.bugs: bug["flags"] = "" @@ -555,8 +562,32 @@ def test_get_recursive_blocking_advisories_builds(self, get_blocks, get_builds): self.assertSetEqual(builds, {"nvr1", "nvr2", "nvr3", "nvr4", "nvr5"}) self.assertEqual(get_blocks.call_count, 3) + @patch("freshmaker.errata.JIRA") + @patch.object(Errata, "_get_jira_issues") + @patch.object(Errata, "_errata_rest_get") + @patch.object(Errata, "_errata_http_get") + def test_is_major_incident_advisory_with_hightouch_bug( + self, errata_http_get, errata_rest_get, get_jira_issues, mocked_jira + ): + mocked_errata = MockedErrataAPI(errata_rest_get, errata_http_get) + mocked_errata.advisory_rest_json["content"]["content"]["cve"] = "" + event = BrewSignRPMEvent("msgid", "libntirpc-1.4.3-4.el7rhgs") + advisories = self.errata.advisories_from_event(event) + self.assertEqual(len(advisories), 1) + self.assertTrue(advisories[0].is_major_incident) + + @patch("freshmaker.errata.JIRA") @patch.object(Errata, "_get_jira_issues") - def test_has_jira_major_incidents(self, get_jira_issues): + @patch.object(Errata, "_errata_rest_get") + @patch.object(Errata, "_errata_http_get") + def test_is_major_incident_advisory_with_major_incident_jira( + self, errata_http_get, errata_rest_get, get_jira_issues, mocked_jira + ): + mocked_errata = MockedErrataAPI(errata_rest_get, errata_http_get) + mocked_errata.bugs = [] + mocked_errata.advisory_rest_json["content"]["content"]["cve"] = "" + event = BrewSignRPMEvent("msgid", "libntirpc-1.4.3-4.el7rhgs") + get_jira_issues.return_value = [ { "id_jira": 123456, @@ -573,38 +604,48 @@ def test_has_jira_major_incidents(self, get_jira_issues): ], } ] - has_major_incidents = self.errata.has_jira_major_incidents("123") - self.assertTrue(has_major_incidents) + mocked_jira.return_value.issue.return_value.fields.issuetype.name = "Vulnerability" + mocked_jira.return_value.issue.return_value.fields.customfield_12324753 = [ + MagicMock(value="Major Incident") + ] - get_jira_issues.return_value = [ + advisories = self.errata.advisories_from_event(event) + self.assertEqual(len(advisories), 1) + self.assertTrue(advisories[0].is_major_incident) + + @patch("freshmaker.errata.JIRA") + @patch.object(Errata, "_get_jira_issues") + @patch.object(Errata, "_errata_rest_get") + @patch.object(Errata, "_errata_http_get") + def test_is_compliance_priority_advisory_with_compliance_priority_bug( + self, errata_http_get, errata_rest_get, get_jira_issues, mocked_jira + ): + mocked_errata = MockedErrataAPI(errata_rest_get, errata_http_get) + mocked_errata.advisory_rest_json["content"]["content"]["cve"] = "" + mocked_errata.bugs = [ { - "id_jira": 123456, - "key": "RHEL-3322", - "summary": "[Minor Incident] CVE-2023-1235 barpack: Heap buffer overflow in Bar Codec [rhel-1.2.3.z]", - "status": "Closed", - "is_private": True, - "labels": [ - "CVE-2023-1235", - "Security", - "SecurityTracking", - "flaw:bz#653422", - "pscomponent:barpack", - ], + "id": 1519778, + "is_security": True, + "alias": "CVE-2017-5753", + "flags": "compliance_priority+,requires_doc_text+,rhsa_sla+", } ] - has_major_incidents = self.errata.has_jira_major_incidents("123") - self.assertFalse(has_major_incidents) - - get_jira_issues.return_value = [] - has_major_incidents = self.errata.has_jira_major_incidents("123") - self.assertFalse(has_major_incidents) - - get_jira_issues.return_value = {"error": "Bad errata id given: 123"} - has_major_incidents = self.errata.has_jira_major_incidents("123") - self.assertFalse(has_major_incidents) + event = BrewSignRPMEvent("msgid", "libntirpc-1.4.3-4.el7rhgs") + advisories = self.errata.advisories_from_event(event) + self.assertEqual(len(advisories), 1) + self.assertTrue(advisories[0].is_compliance_priority) + @patch("freshmaker.errata.JIRA") @patch.object(Errata, "_get_jira_issues") - def test_has_compliance_priority_jira_label(self, get_jira_issues): + @patch.object(Errata, "_errata_rest_get") + @patch.object(Errata, "_errata_http_get") + def test_is_compliance_priority_advisory_with_compliance_priority_jira( + self, errata_http_get, errata_rest_get, get_jira_issues, mocked_jira + ): + mocked_errata = MockedErrataAPI(errata_rest_get, errata_http_get) + mocked_errata.advisory_rest_json["content"]["content"]["cve"] = "" + event = BrewSignRPMEvent("msgid", "libntirpc-1.4.3-4.el7rhgs") + get_jira_issues.return_value = [ { "id_jira": 123456, @@ -613,35 +654,58 @@ def test_has_compliance_priority_jira_label(self, get_jira_issues): "status": "Closed", "is_private": True, "labels": [ - "compliance-priority", + "CVE-2023-1234", + "Security", + "SecurityTracking", + "flaw:bz#653421", + "pscomponent:foopack", ], } ] - has_compliance_priority = self.errata.has_compliance_priority_jira_label("123") - self.assertTrue(has_compliance_priority) + mocked_jira.return_value.issue.return_value.fields.issuetype.name = "Vulnerability" + mocked_jira.return_value.issue.return_value.fields.customfield_12324753 = [ + MagicMock(value="compliance-priority") + ] + + advisories = self.errata.advisories_from_event(event) + self.assertEqual(len(advisories), 1) + self.assertTrue(advisories[0].is_compliance_priority) + + @patch("freshmaker.errata.JIRA") + @patch.object(Errata, "_get_jira_issues") + @patch.object(Errata, "_errata_rest_get") + @patch.object(Errata, "_errata_http_get") + def test_is_contract_priority_advisory_with_compliance_priority_jira( + self, errata_http_get, errata_rest_get, get_jira_issues, mocked_jira + ): + mocked_errata = MockedErrataAPI(errata_rest_get, errata_http_get) + mocked_errata.advisory_rest_json["content"]["content"]["cve"] = "" + event = BrewSignRPMEvent("msgid", "libntirpc-1.4.3-4.el7rhgs") get_jira_issues.return_value = [ { "id_jira": 123456, - "key": "RHEL-3322", - "summary": "CVE-2023-1235 barpack: Heap buffer overflow in Bar Codec [rhel-1.2.3.z]", + "key": "RHEL-3321", + "summary": "CVE-2023-1234 foopack: Heap buffer overflow in Foo Codec [rhel-1.2.3.z]", "status": "Closed", "is_private": True, "labels": [ - "CVE-2023-1235", + "CVE-2023-1234", + "Security", + "SecurityTracking", + "flaw:bz#653421", + "pscomponent:foopack", ], } ] - has_compliance_priority = self.errata.has_compliance_priority_jira_label("123") - self.assertFalse(has_compliance_priority) - - get_jira_issues.return_value = [] - has_compliance_priority = self.errata.has_compliance_priority_jira_label("123") - self.assertFalse(has_compliance_priority) + mocked_jira.return_value.issue.return_value.fields.issuetype.name = "Vulnerability" + mocked_jira.return_value.issue.return_value.fields.customfield_12324753 = [ + MagicMock(value="contract-priority") + ] - get_jira_issues.return_value = {"error": "Bad errata id given: 123"} - has_compliance_priority = self.errata.has_compliance_priority_jira_label("123") - self.assertFalse(has_compliance_priority) + advisories = self.errata.advisories_from_event(event) + self.assertEqual(len(advisories), 1) + self.assertTrue(advisories[0].is_contract_priority) class TestErrataAuthorizedGet(helpers.FreshmakerTestCase): diff --git a/yum-packages.txt b/yum-packages.txt index 893ef3a2..07f63981 100644 --- a/yum-packages.txt +++ b/yum-packages.txt @@ -10,6 +10,7 @@ mod_auth_gssapi mod_ssl python3-devel python3-fedmsg +python3-jira python3-kerberos python3-kobo-rpmlib python3-ldap