def test_make_request(self, ClientSession: AsyncMock): request = ClientSession.return_value.request fake_response = request.return_value.__aenter__.return_value fake_response.json.return_value = {"result": "fake"} api = AsyncErrataAPI("https://errata.example.com") actual = get_event_loop().run_until_complete( api._make_request("HEAD", "/api/path")) request.assert_called_once_with("HEAD", "https://errata.example.com/api/path", headers=api._headers) fake_response.raise_for_status.assert_called_once_with() fake_response.json.assert_awaited_once_with() self.assertEqual(actual, {"result": "fake"}) headers = {"X-Test-Header": "Test Value"} request.reset_mock() fake_response.read.return_value = b"daedbeef" fake_response.raise_for_status.reset_mock() actual = get_event_loop().run_until_complete( api._make_request("GET", "/api/path", headers=headers, parse_json=False)) request.assert_called_once_with("GET", "https://errata.example.com/api/path", headers=headers) fake_response.raise_for_status.assert_called_once_with() fake_response.read.assert_awaited_once_with() self.assertEqual(actual, b"daedbeef")
def test_login(self, SecurityContext: Mock, ClientSession: Mock): client_ctx = SecurityContext.return_value client_ctx.step.return_value = b"faketoken" api = AsyncErrataAPI("https://errata.example.com") get_event_loop().run_until_complete(api.login()) client_ctx.step.assert_called_once_with(b"") self.assertEqual( api._headers["Authorization"], 'Negotiate ' + base64.b64encode(b"faketoken").decode())
def test_get_cves(self, _make_request: Mock, ClientSession: Mock): api = AsyncErrataAPI("https://errata.example.com") api.get_advisory = AsyncMock( return_value={"content": { "content": { "cve": "A B C" } }}) actual = get_event_loop().run_until_complete(api.get_cves(1)) api.get_advisory.assert_awaited_once_with(1) self.assertEqual(actual, ["A", "B", "C"])
def test_create_delete_cve_package_exclusion(self, _make_request: Mock, ClientSession: Mock): api = AsyncErrataAPI("https://errata.example.com") _make_request.return_value = b"" actual = get_event_loop().run_until_complete( api.delete_cve_package_exclusion(100)) _make_request.assert_awaited_once_with( ANY, 'DELETE', '/api/v1/cve_package_exclusion/100', parse_json=False) self.assertEqual(actual, None)
def test_get_advisory(self, _make_request: Mock, ClientSession: Mock): api = AsyncErrataAPI("https://errata.example.com") _make_request.return_value = {"result": "fake"} actual = get_event_loop().run_until_complete(api.get_advisory(1)) _make_request.assert_awaited_once_with(ANY, "GET", "/api/v1/erratum/1") self.assertEqual(actual, {"result": "fake"}) _make_request.reset_mock() actual = get_event_loop().run_until_complete( api.get_advisory("RHBA-2021:0001")) _make_request.assert_awaited_once_with( ANY, "GET", "/api/v1/erratum/RHBA-2021%3A0001") self.assertEqual(actual, {"result": "fake"})
def test_create_cve_package_exclusion(self, _make_request: Mock, ClientSession: Mock): api = AsyncErrataAPI("https://errata.example.com") _make_request.return_value = {"result": "fake"} actual = get_event_loop().run_until_complete( api.create_cve_package_exclusion(1, "CVE-1", "a")) _make_request.assert_awaited_once_with(ANY, 'POST', '/api/v1/cve_package_exclusion', json={ 'cve': 'CVE-1', 'errata': 1, 'package': 'a' }) self.assertEqual(actual, {"result": "fake"})
def __init__(self, runtime: Runtime, use_jira: bool = False, output: str = 'text'): self.runtime = runtime self.use_jira = use_jira bug_tracker_cls = JIRABugTracker if use_jira else BugzillaBugTracker self.config = bug_tracker_cls.get_config(runtime) self.bug_tracker = bug_tracker_cls(self.config) self.target_releases: List[str] = self.config['target_release'] self.product: str = self.config['product'] self.et_data: Dict[str, Any] = runtime.gitdata.load_data( key='erratatool').data self.errata_api = AsyncErrataAPI( self.et_data.get("server", constants.errata_url)) self.problems: List[str] = [] self.output = output
def test_get_builds(self, _make_request: Mock, ClientSession: Mock): api = AsyncErrataAPI("https://errata.example.com") _make_request.return_value = { "ProductVersion1": { "builds": [{ "a-1.0.0-1": {}, "b-1.0.0-1": {} }] }, "ProductVersion2": { "builds": [{ "c-1.0.0-1": {} }] } } actual = get_event_loop().run_until_complete(api.get_builds(1)) _make_request.assert_awaited_once_with( ANY, "GET", "/api/v1/erratum/1/builds_list") self.assertEqual(actual, _make_request.return_value)
async def get_flaws(runtime, advisory, bug_tracker, flaw_bug_tracker, noop): # get attached bugs from advisory advisory_bug_ids = bug_tracker.advisory_bug_ids(advisory) if not advisory_bug_ids: runtime.logger.info(f'Found 0 {bug_tracker.type} bugs attached') return [] attached_tracker_bugs: List[Bug] = bug_tracker.get_tracker_bugs(advisory_bug_ids, verbose=runtime.debug) runtime.logger.info(f'Found {len(attached_tracker_bugs)} {bug_tracker.type} tracker bugs attached: ' f'{sorted([b.id for b in attached_tracker_bugs])}') if not attached_tracker_bugs: return [] # validate and get target_release current_target_release = Bug.get_target_release(attached_tracker_bugs) tracker_flaws, flaw_id_bugs = bug_tracker.get_corresponding_flaw_bugs( attached_tracker_bugs, flaw_bug_tracker, strict=True ) runtime.logger.info(f'Found {len(flaw_id_bugs)} {flaw_bug_tracker.type} corresponding flaw bugs:' f' {sorted(flaw_id_bugs.keys())}') # current_target_release is digit.digit.[z|0] # if current_target_release is GA then run first-fix bug filtering # for GA not every flaw bug is considered first-fix # for z-stream every flaw bug is considered first-fix if current_target_release[-1] == 'z': runtime.logger.info("Detected z-stream target release, every flaw bug is considered first-fix") first_fix_flaw_bugs = list(flaw_id_bugs.values()) else: runtime.logger.info("Detected GA release, applying first-fix filtering..") first_fix_flaw_bugs = [ flaw_bug for flaw_bug in flaw_id_bugs.values() if is_first_fix_any(flaw_bug_tracker, flaw_bug, current_target_release) ] runtime.logger.info(f'{len(first_fix_flaw_bugs)} out of {len(flaw_id_bugs)} flaw bugs considered "first-fix"') if not first_fix_flaw_bugs: return [] runtime.logger.info('Associating CVEs with builds') errata_config = runtime.gitdata.load_data(key='erratatool').data errata_api = AsyncErrataAPI(errata_config.get("server", constants.errata_url)) try: await errata_api.login() await associate_builds_with_cves(errata_api, advisory, attached_tracker_bugs, tracker_flaws, flaw_id_bugs, noop) except ValueError as e: runtime.logger.warn(f"Error associating builds with cves: {e}") finally: await errata_api.close() return first_fix_flaw_bugs
def test_get_cve_package_exclusions(self, _make_request: Mock, ClientSession: Mock): api = AsyncErrataAPI("https://errata.example.com") _make_request.side_effect = lambda _0, _1, _2, params: { 1: { "data": [{ "id": 1 }, { "id": 2 }, { "id": 3 }] }, 2: { "data": [{ "id": 4 }, { "id": 5 }] }, 3: { "data": [] } }[params['page[number]']] async def _call(): items = [] async for item in api.get_cve_package_exclusions(1): items.append(item) return items actual = get_event_loop().run_until_complete(_call()) _make_request.assert_awaited_with(ANY, 'GET', '/api/v1/cve_package_exclusion', params={ 'filter[errata_id]': '1', 'page[number]': 3, 'page[size]': 300 }) self.assertEqual(actual, [{ 'id': 1 }, { 'id': 2 }, { 'id': 3 }, { 'id': 4 }, { 'id': 5 }])
def test_close(self, ClientSession: Mock): api = AsyncErrataAPI("https://errata.example.com") get_event_loop().run_until_complete(api.close()) ClientSession.return_value.close.assert_called_once()
class BugValidator: def __init__(self, runtime: Runtime, use_jira: bool = False, output: str = 'text'): self.runtime = runtime self.use_jira = use_jira bug_tracker_cls = JIRABugTracker if use_jira else BugzillaBugTracker self.config = bug_tracker_cls.get_config(runtime) self.bug_tracker = bug_tracker_cls(self.config) self.target_releases: List[str] = self.config['target_release'] self.product: str = self.config['product'] self.et_data: Dict[str, Any] = runtime.gitdata.load_data( key='erratatool').data self.errata_api = AsyncErrataAPI( self.et_data.get("server", constants.errata_url)) self.problems: List[str] = [] self.output = output async def close(self): await self.errata_api.close() def validate( self, non_flaw_bugs: Iterable[Bug], verify_bug_status: bool, ): non_flaw_bugs = self.filter_bugs_by_release(non_flaw_bugs, complain=True) blocking_bugs_for = self._get_blocking_bugs_for(non_flaw_bugs) self._verify_blocking_bugs(blocking_bugs_for) if verify_bug_status: self._verify_bug_status(non_flaw_bugs) if self.problems: if self.output != 'slack': red_print( "Some bug problems were listed above. Please investigate.") exit(1) green_print( "All bugs were verified. This check doesn't cover CVE flaw bugs.") async def verify_attached_flaws(self, advisory_bugs: Dict[int, List[Bug]]): futures = [] for advisory_id, attached_bugs in advisory_bugs.items(): attached_trackers = [ b for b in attached_bugs if b.is_tracker_bug() ] attached_flaws = [b for b in attached_bugs if b.is_flaw_bug()] futures.append( self._verify_attached_flaws_for(advisory_id, attached_trackers, attached_flaws)) await asyncio.gather(*futures) if self.problems: red_print( "Some bug problems were listed above. Please investigate.") exit(1) green_print("All CVE flaw bugs were verified.") async def _verify_attached_flaws_for(self, advisory_id: int, attached_trackers: Iterable[Bug], attached_flaws: Iterable[Bug]): # Retrieve flaw bugs in Bugzilla for attached_tracker_bugs tracker_flaws, flaw_id_bugs = self.bug_tracker.get_corresponding_flaw_bugs( attached_trackers) # Find first-fix flaws first_fix_flaw_ids = set() if attached_trackers: current_target_release = bzutil.Bug.get_target_release( attached_trackers) if current_target_release[-1] == 'z': first_fix_flaw_ids = flaw_id_bugs.keys() else: first_fix_flaw_ids = { flaw_bug.id for flaw_bug in flaw_id_bugs.values() if bzutil.is_first_fix_any(self.bug_tracker.client( ), flaw_bug, current_target_release) } # Check if attached flaws match attached trackers attached_flaw_ids = {b.id for b in attached_flaws} missing_flaw_ids = attached_flaw_ids - first_fix_flaw_ids if missing_flaw_ids: self._complain( f"On advisory {advisory_id}, {len(missing_flaw_ids)} flaw bugs are not attached but they are referenced by attached tracker bugs: {', '.join(sorted(map(str, missing_flaw_ids)))}." " You probably need to attach those flaw bugs or drop the corresponding tracker bugs." ) extra_flaw_ids = first_fix_flaw_ids - attached_flaw_ids if extra_flaw_ids: self._complain( f"On advisory {advisory_id}, {len(extra_flaw_ids)} flaw bugs are attached but there are no tracker bugs referencing them: {', '.join(sorted(map(str, extra_flaw_ids)))}." " You probably need to drop those flaw bugs or attach the corresponding tracker bugs." ) # Check if advisory is of the expected type advisory_info = await self.errata_api.get_advisory(advisory_id) advisory_type = next(iter(advisory_info["errata"].keys())).upper( ) # should be one of [RHBA, RHSA, RHEA] if not first_fix_flaw_ids: if advisory_type == "RHSA": self._complain( f"Advisory {advisory_id} is of type {advisory_type} but has no first-fix flaw bugs. It should be converted to RHBA or RHEA." ) return # The remaining checks are not needed for a non-RHSA. if advisory_type != "RHSA": self._complain( f"Advisory {advisory_id} is of type {advisory_type} but has first-fix flaw bugs {first_fix_flaw_ids}. It should be converted to RHSA." ) # Check if flaw bugs are associated with specific builds cve_components_mapping: Dict[str, Set[str]] = {} for tracker in attached_trackers: component_name = tracker.whiteboard_component if not component_name: raise ValueError( f"Tracker bug {tracker.id} doesn't have a valid component name in its whiteboard field." ) flaw_ids = tracker_flaws[tracker.id] for flaw_id in flaw_ids: if len(flaw_id_bugs[flaw_id].alias) != 1: raise ValueError( f"Flaw bug {flaw_id} should have exact 1 alias.") cve = flaw_id_bugs[flaw_id].alias[0] cve_components_mapping.setdefault(cve, set()).add(component_name) current_cve_package_exclusions = await AsyncErrataUtils.get_advisory_cve_package_exclusions( self.errata_api, advisory_id) attached_builds = await self.errata_api.get_builds_flattened( advisory_id) expected_cve_packages_exclusions = AsyncErrataUtils.compute_cve_package_exclusions( attached_builds, cve_components_mapping) extra_cve_package_exclusions, missing_cve_package_exclusions = AsyncErrataUtils.diff_cve_package_exclusions( current_cve_package_exclusions, expected_cve_packages_exclusions) for cve, cve_package_exclusions in extra_cve_package_exclusions.items( ): if cve_package_exclusions: self._complain( f"On advisory {advisory_id}, {cve} is not associated with Brew components {', '.join(sorted(cve_package_exclusions))}." " You may need to associate the CVE with the components in the CVE mapping or drop the tracker bugs." ) for cve, cve_package_exclusions in missing_cve_package_exclusions.items( ): if cve_package_exclusions: self._complain( f"On advisory {advisory_id}, {cve} is associated with Brew components {', '.join(sorted(cve_package_exclusions))} without a tracker bug." " You may need to explictly exclude those Brew components from the CVE mapping or attach the corresponding tracker bugs." ) # Check if flaw bugs match the CVE field of the advisory advisory_cves = advisory_info["content"]["content"]["cve"].split() extra_cves = cve_components_mapping.keys() - advisory_cves if extra_cves: self._complain( f"On advisory {advisory_id}, bugs for the following CVEs are already attached but they are not listed in advisory's `CVE Names` field: {', '.join(sorted(extra_cves))}" ) missing_cves = advisory_cves - cve_components_mapping.keys() if missing_cves: self._complain( f"On advisory {advisory_id}, bugs for the following CVEs are not attached but listed in advisory's `CVE Names` field: {', '.join(sorted(missing_cves))}" ) async def get_attached_bugs( self, advisory_ids: Iterable[str]) -> Dict[int, Set[Bug]]: """ Get bugs attached to specified advisories :return: a dict with advisory id as key and set of bug objects as value """ green_print(f"Retrieving bugs for advisory {advisory_ids}") if self.use_jira: issue_keys = { advisory_id: [ issue["key"] for issue in errata.get_jira_issue_from_advisory(advisory_id) ] for advisory_id in advisory_ids } bug_map = self.bug_tracker.get_bugs_map( [key for keys in issue_keys.values() for key in keys]) result = { advisory_id: {bug_map[key] for key in issue_keys[advisory_id]} for advisory_id in advisory_ids } else: advisories = await asyncio.gather(*[ self.errata_api.get_advisory(advisory_id) for advisory_id in advisory_ids ]) bug_map = self.bug_tracker.get_bugs_map( list({ b["bug"]["id"] for ad in advisories for b in ad["bugs"]["bugs"] })) result = { ad["content"]["content"]["errata_id"]: {bug_map[b["bug"]["id"]] for b in ad["bugs"]["bugs"]} for ad in advisories } return result def filter_bugs_by_product(self, bugs): # filter out bugs for different product (presumably security flaw bugs) return [b for b in bugs if b.product == self.product] def filter_bugs_by_release(self, bugs: Iterable[Bug], complain: bool = False) -> List[Bug]: # filter out bugs with an invalid target release filtered_bugs = [] for b in bugs: # b.target release is a list of size 0 or 1 if any(target in self.target_releases for target in b.target_release): filtered_bugs.append(b) elif complain: self._complain( f"bug {b.id} target release {b.target_release} is not in " f"{self.target_releases}. Does it belong in this release?") return filtered_bugs def _get_blocking_bugs_for(self, bugs): # get blocker bugs in the next version for all bugs we are examining candidate_blockers = [b.depends_on for b in bugs if b.depends_on] candidate_blockers = {b for deps in candidate_blockers for b in deps} v = minor_version_tuple(self.target_releases[0]) next_version = (v[0], v[1] + 1) pattern = re.compile(r'^[0-9]+\.[0-9]+\.(0|z)$') # retrieve blockers and filter to those with correct product and target version blocking_bugs = { bug.id: bug for bug in self.bug_tracker.get_bugs(list(candidate_blockers)) # b.target release is a list of size 0 or 1 if any( minor_version_tuple(target) == next_version for target in bug.target_release if pattern.match(target)) and bug.product == self.product } return { bug: [blocking_bugs[b] for b in bug.depends_on if b in blocking_bugs] for bug in bugs } def _verify_blocking_bugs(self, blocking_bugs_for): # complain about blocker bugs that aren't verified or shipped for bug, blockers in blocking_bugs_for.items(): for blocker in blockers: message = str() if blocker.status not in [ 'VERIFIED', 'RELEASE_PENDING', 'CLOSED' ]: if self.output == 'text': message = f"Regression possible: {bug.status} bug {bug.id} is a backport of bug " \ f"{blocker.id} which has status {blocker.status}" elif self.output == 'slack': message = f"`{bug.status}` bug <{bug.weburl}|{bug.id}> is a backport of " \ f"`{blocker.status}` bug <{blocker.weburl}|{blocker.id}>" self._complain(message) if blocker.status == 'CLOSED' and blocker.resolution not in [ 'CURRENTRELEASE', 'NEXTRELEASE', 'ERRATA', 'DUPLICATE', 'NOTABUG', 'WONTFIX' ]: if self.output == 'text': message = f"Regression possible: {bug.status} bug {bug.id} is a backport of bug " \ f"{blocker.id} which was CLOSED {blocker.resolution}" elif self.output == 'slack': message = f"`{bug.status}` bug <{bug.weburl}|{bug.id}> is a backport of bug " \ f"<{blocker.weburl}|{blocker.id}> which was CLOSED `{blocker.resolution}`" self._complain(message) def _verify_bug_status(self, bugs): # complain about bugs that are not yet VERIFIED or more. for bug in bugs: if bug.is_flaw_bug(): continue if bug.status in ["VERIFIED", "RELEASE_PENDING"]: continue if bug.status == "CLOSED" and bug.resolution == "ERRATA": continue status = f"{bug.status}" if bug.status == 'CLOSED': status = f"{bug.status}: {bug.resolution}" self._complain(f"Bug {bug.id} has status {status}") def _complain(self, problem: str): red_print(problem) self.problems.append(problem)