From 92e2c170f32ee3afed4cc69918ce1f3d17eab888 Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Jun 21 2018 00:05:16 +0000 Subject: Convert security data api to a bugzilla api. Turns out we have a race condition with the security data api. It only gets updated an hour or so after the advisory ships and we get triggered. This means that in practice, freshmaker would never trigger since it would always come up with "unknown" for the severity on any event. This moves the code to query bugzilla instead, which is the source of the severity information. --- diff --git a/freshmaker/bugzilla.py b/freshmaker/bugzilla.py new file mode 100644 index 0000000..f62a7ad --- /dev/null +++ b/freshmaker/bugzilla.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Written by Jan Kaluza +# Ralph Bean - -import requests - -from freshmaker import log, conf - - -class SecurityDataAPI(object): - - # Ordered Threat severities. - THREAT_SEVERITIES = [ - "low", - "moderate", - "important", - "critical", - ] - - def __init__(self, server_url=None): - """ - Creates new SecurityDataAPI instance. - - :param str server_url: SecurityDataAPI base URL. - """ - if server_url is not None: - self.server_url = server_url.rstrip('/') - else: - self.server_url = conf.security_data_server_url.rstrip('/') - - def _get_cve(self, cve): - """ - Returns the JSON with metadata about `cve` obtained from - /cve/$cve.json endpoint. - - :param str cve: CVE, for example "CVE-2017-10268". - :rtype: dict - :return: Dict with metadata about CVE. - """ - log.debug("Querying SecurityDataAPI for %s", cve) - r = requests.get("%s/cve/%s.json" % (self.server_url, cve)) - r.raise_for_status() - return r.json() - - def get_highest_threat_severity(self, cve_list): - """ - Fetches metadata about each CVE in `cve_list` and returns the name of - highest severity rate. See `SecurityDataAPI.THREAT_SEVERITIES` for - list of possible severity rates. - - :param list cve_list: List of strings with CVE names. - :rtype: str - :return: Name of highest severity rate occuring in CVEs from `cve_list`. - """ - max_rating = -1 - for cve in cve_list: - try: - data = self._get_cve(cve) - except requests.exceptions.HTTPError as e: - if e.response.status_code == 404: - log.warn( - "CVE %s cannot be found in SecurityDataAPI, " - "threat_severity unknown.", cve) - continue - raise - severity = data["threat_severity"].lower() - try: - rating = SecurityDataAPI.THREAT_SEVERITIES.index(severity) - except ValueError: - log.error("Unknown threat_severity '%s' for CVE %s", - severity, cve) - continue - max_rating = max(max_rating, rating) - - if max_rating == -1: - return None - return SecurityDataAPI.THREAT_SEVERITIES[max_rating] diff --git a/requirements.txt b/requirements.txt index 9fb2493..304f513 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,3 +25,4 @@ dogpile.cache pyldap koji tabulate +lxml diff --git a/tests/test_bugzilla.py b/tests/test_bugzilla.py new file mode 100644 index 0000000..85141be --- /dev/null +++ b/tests/test_bugzilla.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from mock import patch + +from freshmaker.bugzilla import BugzillaAPI +from tests import helpers + + +class MockResponse(object): + def __init__(self, text): + self.text = text + + def raise_for_status(self): + pass + + +xml_with_status = """ + +impact={impact} + +""" + +xml_with_empty_status = """ + + + +""" +xml_without_status = """""" +xml_with_empty_bug = """""" + + +class TestBugzillaAPI(helpers.FreshmakerTestCase): + + @patch("freshmaker.bugzilla.requests.get") + def test_get_highest_impact(self, requests_get): + impacts = ["Low", "Moderate", "Important", "Critical"] + bugzilla = BugzillaAPI() + for num_of_cves in range(1, 4): + requests_get.side_effect = [ + MockResponse(xml_with_status.format(impact=impact)) + for impact in impacts] + ret = bugzilla.get_highest_impact(["CVE-1"] * num_of_cves) + self.assertEqual(ret, impacts[num_of_cves - 1].lower()) + + @patch("freshmaker.bugzilla.requests.get") + def test_get_highest_impact_empty_list(self, requests_get): + bugzilla = BugzillaAPI() + ret = bugzilla.get_highest_impact([]) + self.assertEqual(ret, None) + requests_get.assert_not_called() + + @patch("freshmaker.bugzilla.requests.get") + def test_get_highest_impact_no_status(self, requests_get): + bugzilla = BugzillaAPI() + requests_get.return_value = MockResponse(xml_without_status) + ret = bugzilla.get_highest_impact(["CVE-1"]) + self.assertEqual(ret, None) + + @patch("freshmaker.bugzilla.requests.get") + def test_get_highest_impact_empty_status(self, requests_get): + bugzilla = BugzillaAPI() + requests_get.return_value = MockResponse(xml_with_empty_status) + ret = bugzilla.get_highest_impact(["CVE-1"]) + self.assertEqual(ret, None) + + @patch("freshmaker.bugzilla.requests.get") + def test_get_highest_impact_empty_bug(self, requests_get): + bugzilla = BugzillaAPI() + requests_get.return_value = MockResponse(xml_with_empty_bug) + ret = bugzilla.get_highest_impact(["CVE-1"]) + self.assertEqual(ret, None) + + @patch("freshmaker.bugzilla.requests.get") + def test_get_highest_impact_unknown_impact(self, requests_get): + impacts = ["Low", "unknown"] + requests_get.side_effect = [ + MockResponse(xml_with_status.format(impact=impact)) + for impact in impacts] + bugzilla = BugzillaAPI() + ret = bugzilla.get_highest_impact(["CVE-1", "CVE-2"]) + self.assertEqual(ret, "low") diff --git a/tests/test_errata.py b/tests/test_errata.py index 8e518e1..f63bb59 100644 --- a/tests/test_errata.py +++ b/tests/test_errata.py @@ -160,8 +160,8 @@ class TestErrata(helpers.FreshmakerTestCase): self.errata = Errata("https://localhost/") self.patcher = helpers.Patcher( - 'freshmaker.errata.SecurityDataAPI.') - self.patcher.patch("get_highest_threat_severity", + 'freshmaker.errata.BugzillaAPI.') + self.patcher.patch("get_highest_impact", return_value="moderate") def tearDown(self): diff --git a/tests/test_security_data.py b/tests/test_security_data.py deleted file mode 100644 index f30660d..0000000 --- a/tests/test_security_data.py +++ /dev/null @@ -1,55 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2017 Red Hat, Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from mock import patch - -from freshmaker.security_data import SecurityDataAPI -from tests import helpers - - -class TestSecurityDataAPI(helpers.FreshmakerTestCase): - - @patch("freshmaker.security_data.requests.get") - def test_get_highest_threat_severity(self, requests_get): - severities = ["Low", "Moderate", "Important", "Critical"] - sec_data = SecurityDataAPI() - for num_of_cves in range(1, 4): - requests_get.return_value.json.side_effect = [ - {"threat_severity": severity} for severity in severities] - ret = sec_data.get_highest_threat_severity(["CVE-1"] * num_of_cves) - self.assertEqual(ret, severities[num_of_cves - 1].lower()) - - @patch("freshmaker.security_data.requests.get") - def test_get_highest_threat_severity_empty_list(self, requests_get): - sec_data = SecurityDataAPI() - ret = sec_data.get_highest_threat_severity([]) - self.assertEqual(ret, None) - requests_get.assert_not_called() - - @patch("freshmaker.security_data.requests.get") - def test_get_highest_threat_severity_unknown_severity(self, requests_get): - severities = ["Low", "unknown"] - requests_get.return_value.json.side_effect = [ - {"threat_severity": severity} for severity in severities] - sec_data = SecurityDataAPI() - ret = sec_data.get_highest_threat_severity(["CVE-1", "CVE-2"]) - self.assertEqual(ret, "low")